所以我想贡献一个像 lesmana's 这样的答案,但我认为我的可能是一个更简单、更有利的纯 Bourne-shell 解决方案:
# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.
我认为最好从里到外解释——command1 将执行并在 stdout(文件描述符 1)上打印其常规输出,然后一旦完成,printf 将执行并在其 stdout 上打印 icommand1 的退出代码,但那个 stdout被重定向到文件描述符 3。
当 command1 运行时,它的 stdout 被传送到 command2(printf 的输出永远不会传送到 command2,因为我们将它发送到文件描述符 3 而不是 1,这是管道读取的内容)。然后我们将 command2 的输出重定向到文件描述符 4,这样它也不会出现在文件描述符 1 之外——因为我们希望稍后释放文件描述符 1,因为我们会将文件描述符 3 上的 printf 输出带回到文件描述符中1 - 因为这是命令替换(反引号)将捕获的内容,这就是将被放入变量中的内容。
最后一点神奇之处在于,我们首先将exec 4>&1 作为单独的命令执行 - 它打开文件描述符 4 作为外部 shell 标准输出的副本。命令替换将从其内部命令的角度捕获标准输出中写入的任何内容 - 但由于 command2 的输出就命令替换而言将发送到文件描述符 4,因此命令替换不会捕获它 - 但是一旦它从命令替换中“退出”它实际上仍将转到脚本的整体文件描述符 1。
(exec 4>&1 必须是一个单独的命令,因为当您尝试在命令替换中写入文件描述符时,许多常见的 shell 不喜欢它,这是在使用替换。所以这是最简单的可移植方式。)
你可以用一种不那么技术性和更有趣的方式来看待它,就好像命令的输出是相互跳跃的:command1 管道到 command2,然后 printf 的输出跳过命令 2,这样 command2 就不会捕获它,然后命令 2 的输出跳过并跳出命令替换,就像 printf 及时着陆以被替换捕获,因此它最终出现在变量中,并且命令 2 的输出继续以愉快的方式被写入标准输出,就像在普通管道中一样。
另外,据我了解,$? 仍将包含管道中第二个命令的返回码,因为变量赋值、命令替换和复合命令对于其中的命令的返回码都是有效透明的,因此 command2 的返回状态应该被传播出去 - 这就是为什么我认为这可能是比 lesmana 提出的解决方案更好的解决方案。
根据 lesmana 提到的注意事项,command1 可能会在某些时候最终使用文件描述符 3 或 4,因此为了更健壮,您可以这样做:
exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-
请注意,我在示例中使用了复合命令,但子 shell(使用 ( ) 代替 { } 也可以,但可能效率较低。)
命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符四,而3>&1 后面的复合命令将继承文件描述符三。所以4>&- 确保内部复合命令不会继承文件描述符四,3>&- 不会继承文件描述符三,因此 command1 获得了一个“更干净”、更标准的环境。你也可以将内部的4>&- 移动到3>&- 旁边,但我想为什么不尽可能地限制它的范围。
我不确定直接使用文件描述符 3 和 4 的频率 - 我认为大多数时候程序使用返回未使用的文件描述符的系统调用,但有时代码写入文件描述符 3直接,我猜(我可以想象一个程序检查文件描述符以查看它是否打开,如果是则使用它,或者如果不是,则相应地表现不同)。所以后者可能最好记住并用于通用情况。