stderrへのリダイレクトはbashでは機能しますが、zshでは機能しません。

stderrへのリダイレクトはbashでは機能しますが、zshでは機能しません。

バッシュでは

❯ 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を受信して​​終了します。

プロセスのスケジューリング順序によって異なり、左側のプロセスechoteeインポートプロセスの両方が右側のプロセスの前に実行される場合は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番目の出力ラインは失われます。helloworld

$ (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はteeSIGPIPEとエラーを無視しているようです。

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

おすすめ記事