Warum schlägt der Schnitt mit bash fehl und nicht mit zsh?

10

Ich erstelle eine Datei mit tabulatorgetrennten Feldern.

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

Ich habe das folgende Skript benannt zsh.sh

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

Ich teste es.

$ ./zsh.sh input
bar
bar

Das funktioniert gut. Wenn ich jedoch die erste Zeile ändere, die bashstattdessen aufgerufen wird, schlägt dies fehl.

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

Warum scheitert bashund funktioniert das zsh?

Zusätzliche Fehlerbehebung

  • Die Verwendung direkter Pfade im Shebang anstelle des envgleichen Verhaltens.
  • Das Piping mit echoanstelle des Here-Strings <<<$lineerzeugt ebenfalls das gleiche Verhalten. dh echo $line | cut -f 2.
  • Verwenden awkstatt cut Arbeiten für beide Shells. dh <<<$line awk '{print $2}'.
Sparhawk
quelle
4
By the way, können Sie Ihre Testdatei einfacher machen , indem einer von ihnen tun: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...', oder printf 'foo\tbar\tbaz\n...\n'oder Variationen davon. Sie müssen nicht jeden Tab oder jede neue Zeile einzeln umbrechen.
Bis auf weiteres angehalten.

Antworten:

13

Was passiert ist, dass bashdie Tabulatoren durch Leerzeichen ersetzt werden. Sie können dieses Problem vermeiden, indem Sie "$line"stattdessen sagen oder Leerzeichen explizit ausschneiden.

Michael Vehrs
quelle
1
Gibt es einen Grund, warum Bash a sieht \tund durch ein Leerzeichen ersetzt?
user1717828
@ user1717828 Ja, es heißt Spit + Glob-Operator . Dies passiert, wenn Sie eine Variable verwenden, die in Bash- und ähnlichen Shells nicht in Anführungszeichen steht.
Terdon
1
@terdon, in <<< $line, bashteilt, aber nicht glob. Es gibt keinen Grund, warum es sich hier spalten würde, wenn man <<<ein einziges Wort erwartet. Es teilt sich und verbindet sich dann in diesem Fall, was wenig Sinn macht und gegen alle anderen Shells-Implementierungen ist, die <<<vorher oder nachher unterstützt haben bash. IMO ist es ein Fehler.
Stéphane Chazelas
@ StéphaneChazelas fair genug, das Problem ist sowieso mit dem geteilten Teil.
Terdon
2
@ StéphaneChazelas Kein Split (noch Glob) passiert auf Bash 4.4
17

Das liegt daran, dass in <<< $line, bashWortaufteilung (wenn auch nicht Globbing) aktiviert wird, $lineda es dort nicht zitiert wird, und dann die resultierenden Wörter mit dem Leerzeichen verknüpft wird (und dies in eine temporäre Datei, gefolgt von einem Zeilenumbruchzeichen, einfügt und dies zum Standard macht cut).

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

tabzufällig im Standardwert von $IFS:

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

Die Lösung bashbesteht darin, die Variable in Anführungszeichen zu setzen.

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

Beachten Sie, dass dies die einzige Shell ist, die dies tut. zsh(woher <<<kommt, inspiriert vom Unix-Port von rc) ksh93, mkshund yashdie auch unterstützen, <<<tun es nicht.

Wenn es um die Arrays kommt, mksh, yashund zshkommt auf dem ersten Zeichen $IFS, bashund ksh93auf Platz.

$ 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$

Es gibt einen Unterschied zwischen zsh/ yashund mksh(mindestens Version R52), wenn $IFSes leer ist:

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

Das Verhalten ist über Shells hinweg konsistenter, wenn Sie es verwenden "${a[*]}"(außer dass es mkshimmer noch einen Fehler gibt, wenn $IFSes leer ist).

Dies echo $line | ...ist jedoch der übliche Split + Glob-Operator in allen Bourne-ähnlichen Shells zsh(und die damit verbundenen üblichen Probleme echo).

Stéphane Chazelas
quelle
1
Hervorragende Antwort! Vielen Dank (+1). Ich werde jedoch den Fragesteller mit den niedrigsten Antworten akzeptieren, da sie die Frage perfekt genug beantwortet haben, um meine Dummheit zu offenbaren.
Sparhawk
10

Das Problem ist, dass Sie nicht zitieren $line. Ändern Sie zur Untersuchung die beiden Skripte so, dass sie einfach gedruckt werden $line:

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

und

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

Vergleichen Sie nun ihre Ausgabe:

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

Wie Sie sehen können $line, werden die Registerkarten von bash nicht richtig interpretiert , da Sie nicht zitieren . Zsh scheint damit besser umzugehen. Wird jetzt cutstandardmäßig \tals Feldtrennzeichen verwendet. Da Ihr bashSkript die Registerkarten frisst (aufgrund des Operators split + glob), wird daher cutnur ein Feld angezeigt und es wird entsprechend gehandelt . Was Sie wirklich laufen, ist:

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

Geben Sie Ihre Variable an, damit Ihr Skript in beiden Shells wie erwartet funktioniert:

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

Dann erzeugen beide die gleiche Ausgabe:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
terdon
quelle
Hervorragende Antwort! Vielen Dank (+1). Ich werde jedoch den Fragesteller mit den niedrigsten Antworten akzeptieren, da sie die Frage perfekt genug beantwortet haben, um meine Dummheit zu offenbaren.
Sparhawk
^ Stimmen Sie dafür, die einzige Antwort zu sein (bis jetzt), die tatsächlich das korrigiertebash.sh
lauir
1

Wie bereits beantwortet, besteht eine portablere Möglichkeit, eine Variable zu verwenden, darin, sie in Anführungszeichen zu setzen:

$ 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$

Es gibt einen Unterschied in der Implementierung in Bash, mit der Zeile:

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

Dies ist das Ergebnis der meisten Muscheln:

/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$

Nur Bash teilt die Variable rechts von, <<<wenn sie nicht in Anführungszeichen steht.
Dies wurde jedoch in Bash-Version 4.4 korrigiert. Dies
bedeutet, dass der Wert von $IFSdas Ergebnis von beeinflusst <<<.


Mit der Zeile:

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

Alle Shells verwenden das erste Zeichen von IFS, um Werte zu verbinden.

/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$

Mit "${l[@]}"wird ein Leerzeichen benötigt, um die verschiedenen Argumente zu trennen, aber einige Shells verwenden den Wert von IFS (Ist das richtig?).

/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$

Bei einem Null-IFS sollten die Werte wie bei dieser Zeile verbunden werden:

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$

Aber sowohl lksh als auch mksh tun dies nicht.

Wenn wir zu einer Liste von Argumenten wechseln:

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$

Sowohl yash als auch zsh halten die Argumente nicht getrennt. Ist das ein Fehler?


quelle
Über zsh/ yashund "${l[@]}"in Nicht-Listen-Kontexten ist dies beabsichtigt, wo "${l[@]}"es nur in Listen-Kontexten etwas Besonderes gibt. In Nicht-Listen-Kontexten ist keine Trennung möglich. Sie müssen die Elemente irgendwie verbinden. Das Verbinden mit dem ersten Zeichen von $ IFS ist konsistenter als das Verbinden mit einem Leerzeichen-IMO. dashmacht es auch ( dash -c 'IFS=; a=$@; echo "$a"' x a b). POSIX beabsichtigt jedoch, dieses IIRC zu ändern. Siehe diese (lange) Diskussion
Stéphane Chazelas
Wenn ich mir selbst antworte, nein, wenn ich einen zweiten Blick darauf werfen möchte, lässt POSIX das Verhalten für var=$@nicht spezifiziert.
Stéphane Chazelas