Mengapa cut gagal dengan bash dan bukan zsh?

10

Saya membuat file dengan bidang-bidang yang dibatasi-tab.

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

Saya memiliki skrip berikut bernama zsh.sh

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

Saya mengujinya.

$ ./zsh.sh input
bar
bar

Ini berfungsi dengan baik. Namun, ketika saya mengubah baris pertama bashsebagai gantinya, itu gagal.

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

Mengapa ini gagal bashdan bekerja dengan zsh?

Pemecahan masalah tambahan

  • Menggunakan jalur langsung di shebang bukannya envmenghasilkan perilaku yang sama.
  • Perpipaan dengan echoalih - alih menggunakan string di sini <<<$linejuga menghasilkan perilaku yang sama. yaitu echo $line | cut -f 2.
  • Menggunakan awkalih-alih cut bekerja untuk kedua shell. yaitu <<<$line awk '{print $2}'.
Sparhawk
sumber
4
By the way, Anda dapat membuat file pengujian Anda lebih hanya dengan melakukan salah satu dari ini: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...', atau printf 'foo\tbar\tbaz\n...\n'atau variasi dari ini. Ini menyelamatkan Anda dari keharusan untuk secara individual membungkus setiap tab atau baris baru.
Dijeda sampai pemberitahuan lebih lanjut.

Jawaban:

13

Yang terjadi adalah bashmengganti tab dengan spasi. Anda dapat menghindari masalah ini dengan mengatakan "$line"sebaliknya, atau dengan secara eksplisit memotong spasi.

Michael Vehrs
sumber
1
Apakah ada alasan Bash melihat \tdan menggantinya dengan spasi?
user1717828
@ user1717828 ya, itu disebut operator meludah + glob . Itulah yang terjadi ketika Anda menggunakan variabel yang tidak dikutip dalam bash dan shell yang serupa.
terdon
1
@terdon, dalam <<< $line, bashtidak terpecah tetapi tidak glob. Tidak ada alasan itu akan terpecah di sini karena <<<mengharapkan satu kata. Itu pecah dan kemudian bergabung dalam kasus itu, yang tidak masuk akal dan menentang semua implementasi shell lain yang telah mendukung <<<sebelum atau sesudah bash. IMO itu bug.
Stéphane Chazelas
@ StéphaneChazelas cukup adil, masalahnya adalah dengan bagian yang terpecah pula.
terdon
2
@ StéphaneChazelas Tidak ada perpecahan (atau glob) terjadi pada bash 4.4
17

Itu karena di <<< $line, bashapakah kata splitting, (meskipun tidak globbing) $linekarena tidak dikutip di sana dan kemudian bergabung dengan kata-kata yang dihasilkan dengan karakter spasi (dan menempatkan bahwa dalam file sementara diikuti oleh karakter baris baru dan menjadikannya sebagai stdin dari cut).

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

tabkebetulan berada pada nilai default $IFS:

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

Solusi dengan bashadalah dengan mengutip variabel.

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

Perhatikan bahwa hanya shell yang melakukan itu. zsh(Dari mana <<<datang, terinspirasi oleh port Unix of rc) ksh93, mkshdan yashyang juga mendukung <<<tidak melakukannya.

Ketika datang ke array, mksh, yashdan zshbergabung pada karakter pertama $IFS, bashdan ksh93pada ruang.

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

Ada perbedaan antara zsh/ yashdan mksh(setidaknya versi R52) ketika $IFSkosong:

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

Perilaku ini lebih konsisten di seluruh shell saat Anda gunakan "${a[*]}"(kecuali yang mkshmasih memiliki bug saat $IFSkosong).

Di echo $line | ..., itulah operator split + glob yang biasa ada di semua cangkang mirip Bourne tetapi zsh(dan masalah biasa yang terkait dengannya echo).

Stéphane Chazelas
sumber
1
Jawaban yang sangat bagus! Terima kasih (+1). Saya akan menerima penanya terendah yang sudah diajukan, karena mereka menjawab pertanyaan dengan cukup baik untuk mengungkapkan kebodohan saya.
Sparhawk
10

Masalahnya adalah Anda tidak mengutip $line. Untuk menyelidiki, ubah kedua skrip sehingga mereka cukup mencetak $line:

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

dan

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

Sekarang, bandingkan hasilnya:

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

Seperti yang Anda lihat, karena Anda tidak mengutip $line, tab tidak ditafsirkan dengan benar oleh bash. Zsh tampaknya berurusan dengan itu dengan lebih baik. Sekarang, cutgunakan \tsebagai pembatas bidang secara default. Oleh karena itu, karena bashskrip Anda memakan tab (karena operator glob + split), cuthanya melihat satu bidang dan bertindak sesuai dengannya. Apa yang sebenarnya Anda jalankan adalah:

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

Jadi, agar skrip Anda berfungsi seperti yang diharapkan di kedua shell, kutip variabel Anda:

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

Kemudian, keduanya menghasilkan output yang sama:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
terdon
sumber
Jawaban yang sangat bagus! Terima kasih (+1). Saya akan menerima penanya terendah yang sudah diajukan, karena mereka menjawab pertanyaan dengan cukup baik untuk mengungkapkan kebodohan saya.
Sparhawk
^ memberikan suara untuk menjadi satu-satunya jawaban (sampai sekarang) untuk benar-benar memasukkan yang sudah dikoreksibash.sh
lauir
1

Seperti yang telah dijawab, cara yang lebih portabel untuk menggunakan variabel adalah dengan mengutipnya:

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

Ada perbedaan implementasi dalam bash, dengan baris:

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

Ini adalah hasil dari sebagian besar cangkang:

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

Hanya bash pisahkan variabel di sebelah kanan <<<saat tidak dikutip.
Namun, yang telah diperbaiki pada versi bash 4.4
Itu berarti bahwa nilai $IFSmempengaruhi hasil <<<.


Dengan garis:

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

Semua shell menggunakan karakter pertama IFS untuk menggabungkan nilai.

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

Dengan "${l[@]}", ruang diperlukan untuk memisahkan argumen yang berbeda, tetapi beberapa shell memilih untuk menggunakan nilai dari IFS (Apakah itu benar?).

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

Dengan IFS nol, nilai-nilai harus bergabung, seperti dengan baris ini:

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$

Tetapi baik lksh dan mksh gagal melakukannya.

Jika kami mengubah ke daftar argumen:

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$

Baik yash dan zsh gagal memisahkan argumen. Apakah itu bug?


sumber
Tentang zsh/ yashdan "${l[@]}"dalam konteks non-daftar, itu dengan desain di mana "${l[@]}"hanya khusus dalam konteks daftar. Dalam konteks non-daftar, tidak ada pemisahan yang mungkin terjadi, Anda harus bergabung dengan elemen tersebut entah bagaimana. Bergabung dengan karakter pertama $ IFS lebih konsisten daripada bergabung dengan karakter ruang IMO. dashmelakukannya juga ( dash -c 'IFS=; a=$@; echo "$a"' x a b). Namun POSIX berniat untuk mengubah IIRC itu. Lihat diskusi ini (panjang)
Stéphane Chazelas
Membalas pada diri saya sendiri, tidak, memiliki pandangan kedua, POSIX akan meninggalkan perilaku untuk var=$@tidak ditentukan.
Stéphane Chazelas