为什么`>`重定向不捕获替代进程的标准输出?

Why does `>` redirect not capture substituted processes' stdout?

answer 关于管道和重定向的问题中,罗伯特提到管道还会捕获管道中替代进程的标准输出,而重定向则不会。为什么会这样?到底发生了什么,导致了这种行为:

bash-4.1$ echo -e '1\n2' | tee >(head -n1) >redirect
1
bash-4.1$ cat redirect
1
2
bash-4.1$ echo -e '1\n2' | tee >(head -n1) | cat >pipe
bash-4.1$ cat pipe
1
2
1

我原以为这两种形式会产生相同的结果——后者。

读一个 到另一个问题,重新排序命令中的重定向可能会产生所需的结果似乎是合理的,但无论顺序如何,结果总是相同的:

bash-4.1$ echo -e '1\n2' | tee >redirect >(head -n1)
1
bash-4.1$ cat redirect
1
2
bash-4.1$ echo -e '1\n2' | >redirect tee >(head -n1)
1
bash-4.1$ cat redirect
1
2

为什么 stdout 重定向只影响 tee,但管道也捕获替代进程 head?只需 "By design"?


只是与上述问题相关的一个想法:我认为重定向到文件 管道输出永远没有意义,但它对进程替换确实有意义:

bash-4.1$ echo -e '1\n2\n3' | tee >(head -n1) >(tail -n1) >tee_out | cat >subst_out
bash-4.1$ cat tee_out
1
2
3
bash-4.1$ cat subst_out
1
3

运行 head 的 shell 是由运行 tee 的同一个 shell 生成的,这意味着 teehead 两者为标准输出继承相同的文件描述符,该文件描述符连接到 cat 的管道。这意味着 teehead 都将它们的输出通过管道传输到 cat,从而导致您看到的行为。

为了

echo -e '1\n2' | tee >(head -n1) > redirect

,在 | 之后,只有 tee 的标准输出被重定向到文件,而 head 仍然输出到 tty。要重定向 teehead 的标准输出,您可以编写

echo -e '1\n2' | { tee >(head -n1); } > redirect

{ echo -e '1\n2' | tee >(head -n1); } > redirect

为了

echo -e '1\n2' | tee >(head -n1) | cat > pipe

tee >(head -n1) 作为一个整体,它们的标准输出通过管道传输到 cat。它在逻辑上与 echo -e '1\n2' | { tee >(head -n1); } > redirect.

相同

TL;DR:当执行部分管道时,shell 首先执行 stdin/stdout 的管道重定向>/< 重定向 last。命令替换发生在这两者之间,因此 stdin/stdout 的管道重定向被继承,而 >/< 重定向不是。这是一个设计决定。


公平地说,我接受了 chepner 的回答,因为他是第一个而且他是正确的。但是,我决定通过阅读 bash 的来源添加我自己的答案来记录我理解这个问题的过程,因为 chepner 的答案没有解释为什么 >/< 重定向 isn' t继承。


当 shell 遇到复杂的管道时,了解所涉及的步骤(大大简化)会很有帮助。我已将我原来的问题简化为这个例子:

$ echo x >(echo y) >file
y
$ cat file
x /dev/fd/63

$ echo x >(echo y) | cat >file
$ cat file
x /dev/fd/63
y

仅重定向

当shell遇到echo x >(echo y) >file时,先fork一次执行复杂的命令(有些情况下可以避免,比如builtins),然后fork的shell :

  1. 创建一个 pipe(用于进程替换)
  2. 分叉第二次回声
    1. 分叉:将其 stdin 连接到 pipe[1]
    2. 分叉:exececho yexec'ed echo 继承:
      • stdin 连接到管道[1]
      • 未更改标准输出
  3. 打开 file
  4. 将其 stdout 连接到 file
  5. exececho x /proc/<pid>/fd/<pipe id>exec'ed echo 继承:
    • 标准输入不变
    • stdout 连接到 file

这里,第二个echo继承了分叉shell的stdout,然后分叉shell将其stdout重定向到file。在这种情况下,我认为这种行动顺序没有绝对必要,但我认为这样更有意义。

管道重定向

当 shell 遇到 echo x >(echo y) | cat >file 时,它检测到管道并开始处理它(不分叉):

  1. parent:创建一个pipe(对应完整命令中唯一实际的|
  2. 父级:pipe 左侧的分叉
    1. fork1:将其 stdout 连接到 pipe[0]
    2. fork1:创建一个pipe_subst(用于进程替换)
    3. fork1:分叉第二次回显
      1. nested-fork:将其 stdin 连接到 pipe_subst[1]
      2. 嵌套分叉:exececho yexec'ed echo 继承:
        • stdin 从内叉
        • 连接到 pipe_subst[1]
        • stdout 从外叉
        • 连接到 pipe[0]
    4. fork1:exececho x /proc/<pid>/fd/<pipe_subst id>exec'ed echo 继承:
      • 标准输入不变
      • stdout 连接到 pipe[0]
  3. 父级:pipe 右侧的分叉(同样,有时可以避免这种分叉)
    1. fork2:将其 stdin 连接到 pipe[1]
    2. fork2:打开 file
    3. fork2:将其 stdout 连接到 file
    4. fork2:execcatexec'ed cat 继承:
      • stdin 连接到 pipe[1]
      • stdout 连接到 file

此处,管道优先,即由于管道而导致的 stdin/stdout 重定向在执行管道元素的任何其他操作发生之前执行。因此,echo 都继承了重定向到 cat.

stdout

所有这些实际上是 >file 在进程替换后处理重定向的设计结果。如果在此之前处理了 >file 重定向(就像管道重定向一样),那么 >file 也会被替换的进程继承。