zshではなくbashでカットが失敗するのはなぜですか?


10

タブ区切りフィールドを含むファイルを作成します。

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

次の名前のスクリプトがあります zsh.sh

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

私はそれをテストします。

$ ./zsh.sh input
bar
bar

これは正常に動作します。しかし、最初の行をbash代わりに呼び出すように変更すると、失敗します。

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

なぜこれが失敗しbashて動作するのzshですか?

追加のトラブルシューティング

  • シバンの代わりに直接パスを使用envすると、同じ動作が生成されます。
  • echohere-stringを使用する代わりにでパイピングして<<<$lineも、同じ動作が生成されます。すなわちecho $line | cut -f 2
  • 使用するawk代わりに、cut 作品の両方のシェルのため。すなわち<<<$line awk '{print $2}'

4
ところで、あなたはこれらのいずれかを実行して、より簡単にテストファイルを作成することができますecho -e 'foo\tbar\tbaz\n...'echo $'foo\tbar\tbaz\n...'またはprintf 'foo\tbar\tbaz\n...\n'これらのまたはバリエーション。これにより、各タブまたは改行を個別にラップする必要がなくなります。
追って通知があるまで一時停止。

回答:


13

何が起こるかというとそれはあるbashタブをスペースに置き換えます。この問題を回避するには"$line"、代わりに発声するか、スペースを明示的にカットします。


1
バッシュがa \tを見つけてスペースに置き換える理由はありますか?
user1717828

@ user1717828はい、それはspit + globオペレーターと呼ばれます。これは、bashや類似のシェルで引用符で囲まれていない変数を使用するとどうなるかです。
terdon

1
@terdonは、で<<< $linebash分割ではなく、グロブを行います。<<<単一の単語を想定しているため、ここで分割される理由はありません。その場合、分割してから結合します。これはほとんど意味がなく、<<<beforeまたはafter をサポートしている他のすべてのシェル実装に反していbashます。IMOバグです。
ステファンChazelas

@StéphaneChazelasはかなり公平ですが、問題はとにかく分割部分にあります。
terdon

2
@StéphaneChazelasbash 4.4でスプリット(またはグロブ)が発生しない

17

それは<<< $line、で引用されてbashいないため、で単語分割が行われるためです(グロブではありません)。$line次に、結果の単語をスペース文字で結合します(一時ファイルに改行文字が続き、その標準入力がになりますcut)。

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

tabたまたまデフォルト値にある$IFS

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

の解決策bashは、変数を引用することです。

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

それを行う唯一のシェルであることに注意してください。zsh(ここで<<<のUnixのポートに触発され、から来ているrc、) ksh93mkshyashもサポートする<<<ことをしません。

それは配列になると、mkshyashzshの最初の文字に参加し$IFSbashそしてksh93スペースに。

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

が空の場合、zsh/ yashmksh(少なくともバージョンR52)に$IFSは違いがあります。

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

を使用すると、動作はシェル間でより一貫します"${a[*]}"(空のmksh場合でもバグがあることを除いて$IFS)。

ではecho $line | ...、これはすべてのBourneのようなシェルでの通常のsplit + glob演算子ですzsh(そしてに関連する通常の問題echo)。


1
正解です。ありがとう(+1)。私が愚かさを明らかにするのに十分によく質問に答えたので、私はしかし、最も低い担当者の質問者を受け入れます。
Sparhawk

10

問題は、あなたが引用していないことです$line。調査するには、2つのスクリプトを変更して、単に出力されるようにします$line

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

そして

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

次に、それらの出力を比較します。

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

ご覧のとおり、を引用していないため$line、タブはbashによって正しく解釈されません。Zshはそれをうまく処理するようです。現在、デフォルトでフィールド区切り文字としてcut使用\tされます。したがって、bashスクリプトはタブを食べているため(split + glob演算子のため)、cut1つのフィールドのみが表示され、それに応じて動作します。あなたが実際に実行しているのは:

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

したがって、スクリプトを両方のシェルで期待どおりに動作させるには、変数を引用符で囲みます。

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

次に、どちらも同じ出力を生成します。

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

正解です。ありがとう(+1)。私が愚かさを明らかにするのに十分によく質問に答えたので、私はしかし、最も低い担当者の質問者を受け入れます。
Sparhawk

^実際に修正されたものを含める(まだ)唯一の回答であることへの投票bash.sh
lauir '12

1

すでに答えられているように、変数を使用するよりポータブルな方法は、それを引用することです:

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

bashの実装には、次のような違いがあります。

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

これはほとんどのシェルの結果です:

/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のみが右側の変数を分割します。
ただし、bashバージョン4.4では修正されています。
つまり、の値は$IFSの結果に影響し<<<ます。


次の行で:

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

すべてのシェルは、IFSの最初の文字を使用して値を結合します。

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

では"${l[@]}"、異なる引数を区切るためにスペースが必要ですが、一部のシェルはIFSからの値を使用することを選択します(それで正しいですか?)。

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

IFSがnullの場合、次の行のように、値が結合される必要があります。

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$

しかし、lkshとmkshはどちらも失敗します。

引数のリストに変更した場合:

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$

yashとzshはどちらも引数を分離しておくことができません。それはバグですか?


zsh/ についてyash、および"${l[@]}"リスト以外のコンテキストで"${l[@]}"は、これは仕様によるもので、リストコンテキストでのみ特別です。リスト以外のコンテキストでは、分離することはできません。何らかの方法で要素を結合する必要があります。$ IFSの最初の文字との結合は、スペース文字IMOとの結合よりも一貫しています。dashそれも行います(dash -c 'IFS=; a=$@; echo "$a"' x a b)。ただし、POSIXはそのIIRCを変更する予定です。参照してください。この(長い)の議論
ステファンChazelas


自分への返答、いいえ、再確認すると、POSIXは動作をvar=$@不特定のままにします。
ステファンChazelas
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.