流程替代输出不正常


16

echo one; echo two > >(cat); echo three; 

命令给出了意外的输出。

我读到以下内容:如何在bash中实现流程替换?以及许多其他有关Internet上的流程替换的文章,但不明白为什么它会以这种方式运行。

预期产量:

one
two
three

实际输出:

prompt$ echo one; echo two > >(cat); echo three;
one
three
prompt$ two

另外,从我的角度来看,这两个命令应该等效,但它们不相同:

##### first command - the pipe is used.
prompt$ seq 1 5 | cat
1
2
3
4
5
##### second command - the process substitution and redirection are used.
prompt$ seq 1 5 > >(cat)
prompt$ 1
2
3
4
5

为什么我认为它们应该相同?因为,两者都通过匿名管道-Wikipedia,进程替换seq输出连接到cat输入。

问题:为什么会这样?我的错误在哪里?需要全面的答案(并解释如何bash在后台进行)。


2
即使它不是那么一见钟情清除,它实际上是一个重复的bash等待过程中的替代过程中,即使指令无效
斯特凡Chazelas

2
实际上,最好将另一个问题标记为与此问题重复,因为这个问题更重要。这就是为什么我在这里复制答案的原因。
斯特凡Chazelas

Answers:


21

是的,bash就像ksh(功能来自何处)一样,不等待进程替换内的进程(在脚本中运行下一个命令之前)。

对于<(...)一个,通常如下所示:

cmd1 <(cmd2)

外壳程序将等待,cmd1并且cmd1通常会cmd2由于其读取而等待,直到被替换的管道上的文件结束为止,并且文件结束通常在cmd2死时发生。这就是几个shell(非bash)不用费心等待cmd2in 的相同原因cmd2 | cmd1

对于cmd1 >(cmd2),然而,这通常不是这样的,因为它更cmd2通常等待cmd1一般会退出后,有那么。

这是固定的,zsh因为它在cmd2那里等待(但是如果您将其编写为cmd1 > >(cmd2)cmd1不是内置的,则不行,{cmd1} > >(cmd2)而是按记录使用)。

ksh默认情况下不等待,但是让您使用wait内置命令等待它(它也使pid在中可用$!,尽管这样做对您没有帮助cmd1 >(cmd2) >(cmd3)

rc(使用cmd1 >{cmd2}语法),ksh除了可以使用获取所有后台进程的pids之外$apids

es(也cmd1 >{cmd2})等待cmd2zsh,并且还等待cmd2<{cmd2}过程重定向。

bash确实在中提供了的pid cmd2(或更确切地说,是在该子shell cmd2的子进程中运行的pid,即使它是那里的最后一个命令)$!,但不允许您等待它。

如果必须使用bash,则可以通过使用以下命令来等待两个命令来解决此问题:

{ { cmd1 >(cmd2); } 3>&1 >&4 4>&- | cat; } 4>&1

这使两者cmd1cmd2使其fd 3通向管道。cat将等待最终的文件在另一端,所以通常只退出时都cmd1cmd2都死了。Shell将等待该cat命令。您可能会看到它是一个捕获所有后台进程终止的网络(您可以将其用于在后台启动的其他事情,例如with &,coprocs甚至是后台自身的命令,只要它们不像守护进程那样关闭所有文件描述符)。

请注意,由于上面提到的浪费的subshel​​l进程,即使cmd2关闭fd 3也可以运行(命令通常不执行此操作,但有些命令喜欢sudossh执行)。将来的版本bash可能最终会像其他Shell一样在那里进行优化。然后,您将需要以下内容:

{ { cmd1 >(sudo cmd2; exit); } 3>&1 >&4 4>&- | cat; } 4>&1

为了确保在打开fd 3时还有一个额外的Shell进程,等待该sudo命令。

请注意,它cat不会读取任何内容(因为进程不会在其fd 3上进行写入)。它只是用于同步。它只会执行一个read()系统调用,最后将不返回任何内容。

您实际上可以cat通过使用命令替换执行管道同步来避免运行:

{ unused=$( { cmd1 >(cmd2); } 3>&1 >&4 4>&-); } 4>&1

这一次,它的外壳,而不是cat说从它的另一端是FD 3的开管道读取cmd1cmd2。我们使用的是变量分配,因此中的退出状态cmd1可用$?

或者,您可以手动进行流程替换,然后甚至可以使用系统的sh语法,因为这将成为标准的Shell语法:

{ cmd1 /dev/fd/3 3>&1 >&4 4>&- | cmd2 4>&-; } 4>&1

尽管请注意,如前所述,并非所有sh实现都将cmd1cmd2完成后等待(尽管比其他方法要好)。当时,$?包含的退出状态cmd2;不过bashzsh并使cmd1的退出状态分别在${PIPESTATUS[0]}和中可用$pipestatus[1](另请参见pipefail一些shell中的选项,因此$?可以报​​告除最后一个管道组件之外的管道组件的故障)

请注意,yash其过程重定向功能也有类似的问题。cmd1 >(cmd2)将被写cmd1 /dev/fd/3 3>(cmd2)在那里。但是cmd2不等待,您也不能wait等待它,并且它的pid也不在$!变量中可用。您将使用与相同的解决方法bash


首先,我尝试了echo one; { { echo two > >(cat); } 3>&1 >&4 4>&- | cat; } 4>&1; echo three;,然后将其简化为,echo one; echo two > >(cat) | cat; echo three;并以正确的顺序输出值。所有这些描述符操作3>&1 >&4 4>&-都是必需的吗?另外,我不明白这一点>&4 4>&-我们将重定向stdout到第四个fd,然后关闭第四个fd,然后再次使用4>&1它。为什么需要它以及如何工作?可能是,我应该对此主题提出新的问题吗?
MiniMax '17

1
@MiniMax,但是,这会影响cmd1and 的stdout,与cmd2文件描述符进行少量舞步的目的是还原原始的描述符,并仅使用额外的管道进行等待,而不是同时引导命令的输出。
斯特凡Chazelas

@MiniMax我花了一段时间才明白,我之前并没有把管道的水位降到如此低的水平。最右端4>&1为外括号命令列表创建文件描述符(fd)4,使其等于外括号的stdout。内部支架具有自动设置的stdin / stdout / stderr,以连接到外部支架。但是,3>&1使fd 3连接到外部括号的stdin。>&4使内部括号的stdout连接到外部括号fd 4(我们之前创建的那个)。4>&-从内部花括号关闭fd 4(因为内部花括号的stdout已连接到外部花括号的fd 4)。
Nicholas Pipitone

@MiniMax令人困惑的部分是从右到左的部分,4>&1在其他重定向之前先执行,因此您不要“再次使用4>&1”。总体而言,内部花括号将数据发送到其stdout,该stdout被给定的fd 4覆盖。给出内部括号的fd 4是外部括号的fd 4,它等于外部括号的原始标准输出。
Nicholas Pipitone

Bash感觉像是4>5“ 4到5”,但实际上“ fd 4被fd 5覆盖”。在执行之前,fd 0/1/2被自动连接(与外壳的任何fd一起),您可以根据需要覆盖它们。至少那是我对bash文档的解释。如果您对此有所了解,lmk。
Nicholas Pipitone

4

您可以将第二个命令传递给另一个命令cat,该命令将等待其输入管道关闭。例如:

prompt$ echo one; echo two > >(cat) | cat; echo three;
one
two
three
prompt$

简短而简单。

==========

看起来很简单,但幕后发生了很多事情。如果您对答案的工作方式不感兴趣,则可以忽略其余的答案。

当您拥有时echo two > >(cat); echo three>(cat)它会被交互式shell分叉,并且独立于运行echo two。因此,先echo two完成,然后echo three执行,但要先>(cat)完成。当bash>(cat)意想不到的时间(几毫秒后)获取数据时,它会给您一种类似于提示的情况,您必须按换行键才能返回到终端(就像另一个用户mesg想要您一样)。

但是,给定时echo two > >(cat) | cat; echo three,会生成两个子shell(根据该|符号的文档)。

一个名为A的子shell用于echo two > >(cat),而一个名为B的子shell用于cat。A自动连接到B(A的标准输出是B的标准输入)。然后,echo two>(cat)开始执行。>(cat)的stdout设置为A的stdout,它等于B的stdin。后echo two结束,A出口,关闭其标准输出。但是,>(cat)仍然持有对B的stdin的引用。第二个cat的stdin持有B的stdin,cat直到看到EOF才会退出。仅当没有人在写入模式下打开文件时才给出EOF,因此>(cat)stdout阻止了第二个文件cat。B仍在等待那一秒cat。自echo two退出以来,>(cat)最终获得了EOF,因此>(cat)刷新其缓冲区并退出。没有人再持有B / second cat的标准输入,因此第二个cat读取EOF(B根本不读取其标准输入,这不在乎)。此EOF导致第二个EOF cat刷新其缓冲区,关闭其stdout并退出,然后B退出,因为cat退出并且B在等待cat

需要注意的是,bash还为生成了subshel​​l >(cat)!因此,您会看到

echo two > >(sleep 5) | cat; echo three

echo three即使sleep 5不持有B的标准输入,它仍将等待5秒钟再执行。这是因为生成的隐藏子外壳C >(sleep 5)正在等待sleep,并且C持有B的stdin。你可以看到

echo two > >(exec sleep 5) | cat; echo three

但是不要等,因为sleep它不持有B的stdin,并且没有鬼子shell C持有B的stdin(执行程序将强制睡眠替换C,而不是分叉并使C等待sleep)。不管这个警告,

echo two > >(exec cat) | cat; echo three

如前所述,仍将按顺序正确执行功能。


正如在对我的答案的注释中使用@MiniMax进行的转换中所指出的那样,这具有影响命令标准输出的缺点,并且意味着需要额外读取和写入输出。
斯特凡Chazelas

解释不正确。A我没有等待cat生成>(cat)。正如我在回答中提到的那样,5秒钟后echo two > >(sleep 5 &>/dev/null) | cat; echo three输出的原因three是因为当前版本的bash浪费了一个额外的shell进程在>(sleep 5)等待,sleep而该进程仍然有stdout进入,pipe从而阻止了第二个进程的cat终止。如果用替换它echo two > >(exec sleep 5 &>/dev/null) | cat; echo three以消除该多余的过程,则会发现它会立即返回。
斯特凡Chazelas

它使嵌套的子外壳?我一直在尝试研究bash的实现,以确保echo two > >(sleep 5 &>/dev/null)它至少具有自己的子shell。是否也是未记录的实现细节,sleep 5也会导致获得自己的子外壳?如果有文档记录,那将是用更少的字符完成任务的一种合法方法(除非有一个紧密的循环,我认为没有人会注意到subshel​​l或cat的性能问题)。如果没有记录,那么rip,虽然不错,但在以后的版本中将无法使用。
Nicholas Pipitone

$(...)<(...)确实确实涉及到一个子外壳,但是ksh93或zsh会在同一进程中运行该子外壳中的最后一个命令,这不是bash为什么还有另一个进程在保持管道打开的同时sleep不保持管道打开的原因。的未来版本bash可能会实现类似的优化。
斯特凡Chazelas

1
@StéphaneChazelas我更新了答案,我认为当前对较短版本的解释是正确的,但是您似乎知道Shell的实现细节,因此可以进行验证。我认为应该使用这种解决方案,而不是使用文件描述符舞蹈,因为即使在之下exec,它也能按预期工作。
Nicholas Pipitone
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.