Dlaczego cięcie nie działa z bash, a nie zsh?

10

Tworzę plik z polami rozdzielanymi tabulatorami.

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

Mam następujący skrypt o nazwie zsh.sh

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

Testuję to.

$ ./zsh.sh input
bar
bar

To działa dobrze. Jednak gdy zmienię pierwszy wiersz, aby bashzamiast tego wywoływał , nie powiedzie się.

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

Dlaczego to nie bashdziała i działa z zsh?

Dodatkowe rozwiązywanie problemów

  • Użycie bezpośrednich ścieżek w shebang zamiast envpowoduje takie samo zachowanie.
  • Pipingowanie z echozamiast ciągów tutaj <<<$linerównież powoduje takie samo zachowanie. tj echo $line | cut -f 2.
  • Używanie awkzamiast cut działa dla obu powłok. tj <<<$line awk '{print $2}'.
Krogulec
źródło
4
Nawiasem mówiąc, można dokonać plik testowy prościej, wykonując jedną z nich: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...'lub printf 'foo\tbar\tbaz\n...\n'lub odmiany tych. Pozwala to uniknąć konieczności indywidualnego zawijania każdej karty lub nowej linii.
Wstrzymano do odwołania.

Odpowiedzi:

13

To, co się dzieje, bashzastępuje tabulatory spacjami. Możesz uniknąć tego problemu, mówiąc w "$line"zamian lub jawnie ograniczając spacje.

Michael Vehrs
źródło
1
Czy jest jakiś powód, dla którego Bash widzi a \ti zamienia go na spację?
user1717828,
@ user1717828 tak, nazywa się to operatorem glob spit + . Tak się dzieje, gdy używasz zmiennej niecytowanej w bash i podobnych powłokach.
terdon
1
@terdon, w <<< $line, bashdzieli, ale nie glob. Nie ma powodu, dla którego podzieliłby się tutaj, jak <<<oczekuje się jednego słowa. W tym przypadku dzieli się, a następnie łączy, co nie ma większego sensu i jest sprzeczne ze wszystkimi innymi implementacjami powłok, które były obsługiwane <<<wcześniej lub później bash. IMO to błąd.
Stéphane Chazelas,
@ StéphaneChazelas wystarczy, problem jest z częścią podzieloną.
terdon
2
@ StéphaneChazelas Brak podziału (ani glob) na bash 4.4
17

Wynika to z faktu, że in <<< $line, bashdzielenie wyrazów (choć nie globowanie) jest włączone, $lineponieważ nie jest tam cytowane, a następnie łączy wynikowe słowa ze znakiem spacji (i umieszcza to w pliku tymczasowym, po którym następuje znak nowej linii i robi to od początku cut).

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

tabzdarza się, że ma domyślną wartość $IFS:

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

Rozwiązaniem bashjest zacytowanie zmiennej.

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

Zauważ, że jest to jedyna powłoka, która to robi. zsh(skąd <<<pochodzi, zainspirowany uniksowym portem rc) ksh93, mksha yashktóre także wsparcie <<<tego nie robią.

Kiedy przychodzi do tablic mksh, yashi zshdołącz na pierwszy znak $IFS, basha ksh93na przestrzeni.

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

Istnieje różnica między zsh/ yashi mksh(przynajmniej wersją R52), gdy $IFSjest pusta:

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

Zachowanie jest bardziej spójne podczas używania powłok "${a[*]}"(z wyjątkiem tego, że mkshnadal występuje błąd, gdy $IFSjest pusty).

W echo $line | ..., to zwykły operator podzielonego + glob we wszystkich Bourne'a jak muszle, ale zsh(i zwykłych problemów związanych z echo).

Stéphane Chazelas
źródło
1
Doskonała odpowiedź! Dziękuję (+1). Przyjmę jednak pytającego o najniższej liczbie odpowiedzi, ponieważ odpowiedzieli na to pytanie wystarczająco dobrze, by ujawnić moją głupotę.
Sparhawk
10

Problem polega na tym, że nie cytujesz $line. Aby to sprawdzić, zmień dwa skrypty, aby po prostu wydrukowały $line:

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

i

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

Teraz porównaj ich wyniki:

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

Jak widać, ponieważ nie cytujesz $line, zakładki nie są poprawnie interpretowane przez bash. Zsh wydaje się, że radzi sobie z tym lepiej. Teraz domyślnie cutużywa \tjako ogranicznika pola. Dlatego, ponieważ twój bashskrypt zjada karty (z powodu operatora split + glob), cutwidzi tylko jedno pole i działa odpowiednio. To, co naprawdę biegasz, to:

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

Tak więc, aby skrypt działał zgodnie z oczekiwaniami w obu powłokach, podaj swoją zmienną:

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

Następnie oba generują tę samą wydajność:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
terdon
źródło
Doskonała odpowiedź! Dziękuję (+1). Przyjmę jednak pytającego o najniższej liczbie odpowiedzi, ponieważ odpowiedzieli na to pytanie wystarczająco dobrze, by ujawnić moją głupotę.
Sparhawk
^ głosuj na bycie jedyną odpowiedzią (jak dotąd), która faktycznie zawiera poprawionebash.sh
lauir
1

Jak już wspomniano, bardziej przenośnym sposobem użycia zmiennej jest zacytowanie jej:

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

Istnieje różnica w implementacji w bash, z linią:

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

Jest to wynik większości powłok:

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

Tylko bash dzieli zmienną po prawej stronie, <<<gdy nie jest cytowany.
Zostało to jednak poprawione w wersji bash 4.4.
Oznacza to, że wartość parametru $IFSwpływa na wynik <<<.


Z linią:

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

Wszystkie powłoki używają pierwszego znaku IFS do łączenia wartości.

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

W "${l[@]}"przypadku oddzielenia różnych argumentów potrzebna jest spacja, ale niektóre powłoki wybierają wartość z IFS (czy to prawda?).

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

Przy zerowym IFS wartości powinny zostać połączone, podobnie jak w tym wierszu:

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$

Ale zarówno lksh, jak i mksh tego nie robią.

Jeśli przejdziemy do listy argumentów:

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$

Zarówno yash, jak i zsh nie rozdzielają argumentów. Czy to błąd?


źródło
O zsh/ yashi "${l[@]}"w kontekście innym niż lista, to zgodnie z projektem, gdzie "${l[@]}"jest wyjątkowy tylko w kontekście listy. W kontekstach nie listowych nie ma możliwości separacji , musisz jakoś połączyć elementy. Łączenie z pierwszym znakiem $ IFS jest bardziej spójne niż łączenie ze znakiem spacji IMO. dashrobi to również ( dash -c 'IFS=; a=$@; echo "$a"' x a b). Jednak POSIX zamierza zmienić ten IIRC. Zobacz tę (długą) dyskusję
Stéphane Chazelas,
Odpowiadając sobie: nie, po drugim spojrzeniu POSIX pozostawi to zachowanie jako var=$@nieokreślone.
Stéphane Chazelas,