
バッシュでは
❯ echo "hello" 1>&2 | echo "world"
hello
world
zshでは
❯ echo "hello" 1>&2 | echo "world"
world
私はこの問題を解決するのではなく、なぜこれが起こるのかを理解しようとしています。ここで動作するメカニズムは何ですか?
答え1
これは、zshがマルチリダイレクトを許可する機能であるMULTIOSを実装する方法に関連しているようです。たとえば、実行すると
echo hello > abc > def
内部的には、出力を次のように変換して出力を2つのファイルにコピーします。
echo hello | tee abc def >/dev/null
行為
echo "hello" 1>&2 | echo "world"
だから似ている
echo "hello" | tee /dev/stderr | echo "world"
実際にGNU coreuitlsを使用するBashでもworld
実際には印刷のみします。tee
とにかく私のDebianシステムで。ほとんどの場合。ここで注意すべき点は、echo
入力を読み取らず、tee
左側のパイプから入力を取得して他のパイプに書き込むよりも早く終了できることです。その後、tee
パイプへの書き込みが開始されると、SIGPIPE を受信して終了します。しかし、これはゲームです。
つまり、イベントの順序は次のとおりです。
- どちらも
echo
印刷した内容を印刷し、両方を終了します。 tee
パイプを読み書きhello
しようとします。- SIGPIPEを受信して終了します。
プロセスのスケジューリング順序によって異なり、左側のプロセスecho
とtee
インポートプロセスの両方が右側のプロセスの前に実行される場合はecho
問題ありません。それはまたtee
パイプに最初に書くことに依存しますが、それはtee
私が持っているGNUバージョンとzshで起こるようです。
調べてみると、fd 1が最初に作成されてから終了するstrace
ことがわかります。tee
$ echo "hello" | strace tee /dev/stderr | echo "world"
...
open("/dev/stderr", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
read(0, "hello\n", 8192) = 6
write(1, "hello\n", 6) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=20498, si_uid=1000} ---
+++ killed by SIGPIPE +++
zshに似ています。ここでは正しいプロセスを追跡するのが難しく、シェル全体を追跡すると、関連するすべてのプロセスのシステムコールが提供されます(すれ違い)。とにかくそれを読むプロセスがあり、hello
すぐにどこかに書き込もうとし、SIGPIPEを取得するプロセスがあります。
$ strace -f zsh -c 'echo "hello" 1>&2 | echo "world"'
...
[pid 20503] read(14, "hello\n", 4092) = 6
[pid 20503] write(13, "hello\n", 6) = -1 EPIPE (Broken pipe)
[pid 20503] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=20503, si_uid=1000} ---
[pid 20503] +++ killed by SIGPIPE +++
macOSでは、上記のパイプはsumをtee /dev/stderr
提供しますが、たとえば2番目の出力ラインは失われます。hello
world
$ (echo abc; sleep 2; echo def) | tee /dev/stderr | false
abc
これは、最初にtee
書き込みを行った後、書き込みパイプエラーが原因で終了し、2番目の行を書き込めなくなることと/dev/stderr
一致します。しかし、strace
詳細を見るための同様のツールがあるかどうかはわかりません。
read
ここで、最初の行は読んだ後は問題なく渡されますが、2番目の行は再び失われます。
$ zsh -c '(echo abc; sleep 2; echo def) 1>&2 | read'
abc
GNUのマニュアルページでは、パイプ書き込みエラーの終了についてもtee
言及されています。tee
指定されていない場合、デフォルトの操作は
--output-error
パイプへの書き込み中にエラーが発生した場合は直ちに終了し、非パイプライン出力への書き込み中にエラーを診断することです。
オプションを設定したら、SIGPIPEを無視してエラーを克服して続行します。
$ echo "hello" | tee --output-error=warn /dev/stderr | echo "world"
world
tee: 'standard output': Broken pipe
hello
一方、Busyboxはtee
SIGPIPEとエラーを無視しているようです。
$ echo "hello" | strace busybox tee /dev/stderr | echo "world"
world
...
rt_sigaction(SIGPIPE, {sa_handler=SIG_IGN, sa_mask=[PIPE], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x412030}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
openat(AT_FDCWD, "/dev/stderr", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
read(0, "hello\n", 1024) = 6
write(1, "hello\n", 6) = -1 EPIPE (Broken pipe)
--- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=11700, si_uid=1000} ---
write(3, "hello\n", 6hello
) = 6
read(0, "", 1024) = 0
exit_group(0) = ?
+++ exited with 0 +++
とにかく、入力を読み取らないターゲットにパイプを接続するのはおそらく少し愚かなことです。どちらecho
も独立して実行されます。
echo "hello" 1>&2 & echo "world"