重定向到 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

它通过将输出内部转换为如下内容来将输出复制到两个文件:

echo hello | tee abc def >/dev/null

正在做

echo "hello" 1>&2 | echo "world"

那么类似于

echo "hello" | tee /dev/stderr | echo "world"

实际上,即使在带有 GNU coreuitls 的worldBash 中,它实际上也只能打印。tee无论如何,在我的 Debian 系统上。大多数时候。这里要注意的是,它echo不会读取任何输入,并且可能会快速退出,比tee从左侧管道获取输入并将其写入另一个管道的速度更快。然后,当tee开始写入管道时,它会收到 SIGPIPE 并终止。但这是一场比赛。

也就是说,事件的顺序是这样的:

  • 两者都echo打印它们打印的内容,并且都退出
  • tee读取hello并尝试写入管道
  • 它收到 SIGPIPE 并退出。

这取决于进程的调度顺序,如果左侧进程echotee获取进程都在右侧进程之前运行echo,则没有问题。它还取决于tee首先写入管道,但这似乎是tee我拥有的 GNU 版本和 zsh 发生的情况。

检查后strace我们看到tee首先写入 fd 1 然后死亡:

$ 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 类似。在这里跟踪正确的进程比较困难,并且跟踪整个 shell 给出了所有相关进程的系统调用(交错的)。无论如何,有一个进程读取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 上,上面的管道为tee /dev/stderr我提供了helloworld,但是例如这会丢失第二个输出行:

$ (echo abc; sleep 2; echo def) | tee /dev/stderr | false
abc

这与首先tee​​写入/dev/stderr,然后因写入管道失败而死亡,然后无法写入第二行是一致的。但我不知道是否有类似的工具strace可以查看详细信息。

read在这里,第一行自读取以来就顺利通过,但第二行再次丢失:

$ 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

另一方面,Busyboxtee似乎只是忽略 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"

相关内容