Perché il taglio fallisce con bash e non con zsh?


10

Creo un file con campi delimitati da tabulazioni.

echo foo$'\t'bar$'\t'baz$'\n'foo$'\t'bar$'\t'baz > input

Ho il seguente script chiamato zsh.sh

#!/usr/bin/env zsh
while read line; do
    <<<$line cut -f 2
done < "$1"

L'ho provato.

$ ./zsh.sh input
bar
bar

Funziona benissimo. Tuttavia, quando cambio invece la prima riga per invocare bash, fallisce.

$ ./bash.sh input
foo bar baz
foo bar baz

Perché questo fallisce bashe funziona con zsh?

Ulteriore risoluzione dei problemi

  • L'uso di percorsi diretti nello shebang invece di envprodurre lo stesso comportamento.
  • Anche il piping echoanziché utilizzare la stringa here <<<$lineproduce lo stesso comportamento. vale a dire echo $line | cut -f 2.
  • Usare al awkposto di cut funziona per entrambe le shell. vale a dire <<<$line awk '{print $2}'.

4
Tra l'altro, è possibile rendere il vostro file di prova più semplicemente facendo una di queste: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...', o printf 'foo\tbar\tbaz\n...\n'o variazioni di questi. Ti evita di dover avvolgere individualmente ogni scheda o riga.
In pausa fino a ulteriore avviso.

Risposte:


13

Quello che succede è che bashsostituisce le schede con spazi. Puoi evitare questo problema dicendo "$line"invece o tagliando esplicitamente gli spazi.


1
C'è qualche ragione per cui Bash vede un \te lo sostituisce con uno spazio?
user1717828

@ user1717828 sì, si chiama spit + operatore glob . È ciò che accade quando si utilizza una variabile non quotata in bash e shell simili.
terdon

1
@terdon, in <<< $line, bashsi divide ma non glob. Non c'è motivo per cui si dividerebbe qui perché si <<<aspetta una sola parola. Si divide e si unisce in quel caso, il che ha poco senso ed è contro tutte le altre implementazioni di shell che hanno supportato <<<prima o dopo bash. IMO è un bug.
Stéphane Chazelas,

@ StéphaneChazelas abbastanza giusto, il problema è comunque con la parte divisa.
Terdon

2
@ StéphaneChazelas Non si verifica alcuna divisione (né glob) su bash 4.4

17

Questo perché in <<< $line, la bashsuddivisione delle parole, (anche se non sconvolgente) in $linequanto non è citata lì e quindi unisce le parole risultanti con il carattere spazio (e lo inserisce in un file temporaneo seguito da un carattere di nuova riga e lo rende lo stdin di cut).

$ a=a,b,,c bash -c 'IFS=","; sed -n l <<< $a'
a b  c$

tabsembra essere nel valore predefinito di $IFS:

$ a=$'a\tb'  bash -c 'sed -n l <<< $a'
a b$

La soluzione con bashè di citare la variabile.

$ a=$'a\tb' bash -c 'sed -n l <<< "$a"'
a\tb$

Nota che è l'unica shell che lo fa. zsh(da dove <<<viene, ispirato alla porta Unix di rc) ksh93, mkshe yashche supportano anche <<<non farlo.

Quando si tratta di array, mksh, yashe zshunirsi al primo carattere di $IFS, bashe ksh93sullo spazio.

$ mksh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ yash -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ ksh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ bash -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1 2$

C'è una differenza tra zsh/ yashe mksh(versione R52 almeno) quando $IFSè vuoto:

$ mksh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
12$

Il comportamento è più coerente tra le shell quando si utilizza "${a[*]}"(tranne che mkshha ancora un bug quando $IFSè vuoto).

In echo $line | ..., questo è il solito operatore split + glob in tutte le shell tipo Bourne ma zsh(e i soliti problemi associati echo).


1
Risposta eccellente! Grazie (+1). Accetterò comunque l'interrogatore con il punteggio più basso, poiché hanno risposto alla domanda abbastanza bene da rivelare la mia stupidità.
Sparhawk,

10

Il problema è che non stai citando $line. Per indagare, modifica i due script in modo che stampino semplicemente $line:

#!/usr/bin/env bash
while read line; do
    echo $line
done < "$1"

e

#!/usr/bin/env zsh
while read line; do
    echo $line
done < "$1"

Ora confronta il loro output:

$ bash.sh input 
foo bar baz
foo bar baz
$ zsh.sh input 
foo    bar    baz
foo    bar    baz

Come puoi vedere, poiché non stai citando $line, le schede non sono interpretate correttamente da bash. Zsh sembra affrontarlo meglio. Ora, cututilizza \tcome delimitatore di campo per impostazione predefinita. Pertanto, poiché il tuo bashscript sta mangiando le schede (a causa dell'operatore split + glob), cutvede solo un campo e agisce di conseguenza. Quello che stai veramente correndo è:

$ echo "foo bar baz" | cut -f 2
foo bar baz

Quindi, per far funzionare il tuo script come previsto in entrambe le shell, cita la tua variabile:

while read line; do
    <<<"$line" cut -f 2
done < "$1"

Quindi, entrambi producono lo stesso output:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar

Risposta eccellente! Grazie (+1). Accetterò comunque l'interrogatore con il punteggio più basso, poiché hanno risposto alla domanda abbastanza bene da rivelare la mia stupidità.
Sparhawk,

^ voto per essere l'unica risposta (ancora) per includere effettivamente il correttobash.sh
lauir

1

Come è già stato risposto, un modo più portatile di usare una variabile è di citarla:

$ printf '%s\t%s\t%s\n' foo bar baz
foo    bar    baz
$ l="$(printf '%s\t%s\t%s\n' foo bar baz)"
$ <<<$l     sed -n l
foo bar baz$

$ <<<"$l"   sed -n l
foo\tbar\tbaz$

C'è una differenza di implementazione in bash, con la linea:

l="$(printf '%s\t%s\t%s\n' foo bar baz)"; <<<$l  sed -n l

Questo è il risultato della maggior parte delle shell:

/bin/sh         : foo bar baz$
/bin/b43sh      : foo bar baz$
/bin/bash       : foo bar baz$
/bin/b44sh      : foo\tbar\tbaz$
/bin/y2sh       : foo\tbar\tbaz$
/bin/ksh        : foo\tbar\tbaz$
/bin/ksh93      : foo\tbar\tbaz$
/bin/lksh       : foo\tbar\tbaz$
/bin/mksh       : foo\tbar\tbaz$
/bin/mksh-static: foo\tbar\tbaz$
/usr/bin/ksh    : foo\tbar\tbaz$
/bin/zsh        : foo\tbar\tbaz$
/bin/zsh4       : foo\tbar\tbaz$

Bash divide solo la variabile a destra <<<quando non quotata.
Tuttavia, ciò è stato corretto sulla versione 4.4 di bash
Ciò significa che il valore di $IFSinfluenza il risultato di <<<.


Con la linea:

l=(1 2 3); IFS=:; sed -n l <<<"${l[*]}"

Tutte le shell usano il primo carattere di IFS per unire i valori.

/bin/y2sh       : 1:2:3$
/bin/sh         : 1:2:3$
/bin/b43sh      : 1:2:3$
/bin/b44sh      : 1:2:3$
/bin/bash       : 1:2:3$
/bin/ksh        : 1:2:3$
/bin/ksh93      : 1:2:3$
/bin/lksh       : 1:2:3$
/bin/mksh       : 1:2:3$
/bin/zsh        : 1:2:3$
/bin/zsh4       : 1:2:3$

Con "${l[@]}", è necessario uno spazio per separare i diversi argomenti, ma alcune shell scelgono di usare il valore da IFS (è corretto?).

/bin/y2sh       : 1:2:3$
/bin/sh         : 1 2 3$
/bin/b43sh      : 1 2 3$
/bin/b44sh      : 1 2 3$
/bin/bash       : 1 2 3$
/bin/ksh        : 1 2 3$
/bin/ksh93      : 1 2 3$
/bin/lksh       : 1:2:3$
/bin/mksh       : 1:2:3$
/bin/zsh        : 1:2:3$
/bin/zsh4       : 1:2:3$

Con un IFS null, i valori devono essere uniti, come con questa riga:

a=(1 2 3); IFS=''; sed -n l <<<"${a[*]}"

/bin/y2sh       : 123$
/bin/sh         : 123$
/bin/b43sh      : 123$
/bin/b44sh      : 123$
/bin/bash       : 123$
/bin/ksh        : 123$
/bin/ksh93      : 123$
/bin/lksh       : 1 2 3$
/bin/mksh       : 1 2 3$
/bin/zsh        : 123$
/bin/zsh4       : 123$

Ma sia lksh che mksh non riescono a farlo.

Se passiamo a un elenco di argomenti:

l=(1 2 3); IFS=''; sed -n l <<<"${l[@]}"

/bin/y2sh       : 123$
/bin/sh         : 1 2 3$
/bin/b43sh      : 1 2 3$
/bin/b44sh      : 1 2 3$
/bin/bash       : 1 2 3$
/bin/ksh        : 1 2 3$
/bin/ksh93      : 1 2 3$
/bin/lksh       : 1 2 3$
/bin/mksh       : 1 2 3$
/bin/zsh        : 123$
/bin/zsh4       : 123$

Sia yash che zsh non riescono a mantenere separati gli argomenti. È un bug?


Informazioni su zsh/ yashe "${l[@]}"in un contesto non di elenco, questo è di progettazione dove "${l[@]}"è speciale solo nei contesti di elenco. In contesti non di elenco, non è possibile alcuna separazione , è necessario in qualche modo unire gli elementi. L'unione con il primo personaggio di $ IFS è più coerente rispetto all'unione con un carattere spazio IMO. dashlo fa anche ( dash -c 'IFS=; a=$@; echo "$a"' x a b). Tuttavia POSIX intende cambiare tale IIRC. Vedi questa (lunga) discussione
Stéphane Chazelas,


Rispondendo a me stesso, no, dando una seconda occhiata, POSIX lascerà il comportamento per var=$@non specificato.
Stéphane Chazelas,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.