在Bash中管道输出和捕获退出状态

我想在Bash中执行一个长时间的运行命令,并且捕获它的退出状态,并且开始输出。

所以我这样做:

command | tee out.txt ST=$? 

问题在于变量ST捕获了tee的退出状态而不是命令。 我该如何解决这个问题?

请注意,命令是长时间运行,并将输出重定向到一个文件,稍后查看它不是一个好的解决方案。

有一个名为$PIPESTATUS的内部Bash变量; 它是一个数组,用于保存命令的最后一个前台管道中每个命令的退出状态。

 <command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0 

或者也可以与其他shell(如zsh)一起使用的另一种方法是启用pipefail:

 set -o pipefail ... 

由于语法有点不同,第一个选项不适用于zsh

使用bash的set -o pipefail是有帮助的

pipefail:管道的返回值是以非零状态退出的最后一个命令的状态,如果不存在以非零状态退出的命令,则为零

哑解决方案:通过命名管道(mkfifo)连接它们。 然后该命令可以运行第二。

  mkfifo pipe tee out.txt < pipe & command > pipe echo $? 

有一个数组可以给你管道中每个命令的退出状态。

 $ cat x| sed 's///' cat: x: No such file or directory $ echo $? 0 $ cat x| sed 's///' cat: x: No such file or directory $ echo ${PIPESTATUS[*]} 1 0 $ touch x $ cat x| sed 's' sed: 1: "s": substitute pattern can not be delimited by newline or backslash $ echo ${PIPESTATUS[*]} 0 1 

此解决方案不使用bash特定功能或临时文件。 奖金:最后退出状态实际上是退出状态,而不是文件中的一些字符串。

情况:

 someprog | filter 

你想要someprog的退出状态和someprog的输出。

这是我的解决方案:

 ((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1 echo $? 

在unix.stackexchange.com上查看我对同一问题的回答,以获得有关如何工作的详细说明以及一些注意事项。

通过将PIPESTATUS[0]和在子shell中执行exit命令的结果相结合,可以直接访问初始命令的返回值:

command | tee ; ( exit ${PIPESTATUS[0]} )

这是一个例子:

 # the "false" shell built-in command returns 1 false | tee ; ( exit ${PIPESTATUS[0]} ) echo "return value: $?" 

会给你:

return value: 1

所以我想提供一个像lesmana的答案,但是我认为我可能是一个更简单,更有利的纯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. 

我认为这是从内到外的最好解释 – 命令1将执行并输出标准输出(文件描述符1),然后一旦完成,printf将执行并打印icommand1的退出代码的标准输出,但该标准输出重定向到文件描述符3。

当command1正在运行时,它的stdout被传送到command2(printf的输出从不会将它传给command2,因为我们把它发送到文件描述符3而不是1,这是管道读取的内容)。 然后,我们将command2的输出重定向到文件描述符4,以便它也停留在文件描述符1之外 – 因为我们稍后想要文件描述符1是空闲的,因为我们将把文件描述符3上的printf输出返回到文件描述符1 – 因为这就是命令替换(反引号),将被捕获,这将被放置到变量中。

最后一点就是第一个exec 4>&1我们做了一个单独的命令 – 它打开文件描述符4作为外壳的标准输出的副本。 命令替换将从其内部命令的角度捕获任何写在标准输出上的命令 – 但是由于命令替换所涉及的命令2的输出将转到文件描述符4,所以命令替换不会捕获它 – 但是一旦它得到命令替换的“出”,它仍然有效地进入脚本的整个文件描述符1。

exec 4>&1必须是一个独立的命令,因为当你尝试在命令替换中写入一个文件描述符时,许多常见的shell不喜欢它,这是在使用替换的“external”命令中打开的。所以这是最简单的便携方式。)

你可以用更少的技术和更有趣的方式来看待它,就好像这些命令的输出是相互超越的:命令1管道到命令2,然后printf的输出跳过命令2,以使命令2不捕捉它,然后命令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>&-的内部4>&- ,但我想为什么不尽可能地限制它的范围。

我不知道多长时间直接使用文件描述符三和四 – 我认为大多数时间程序使用系统调用返回未使用的文件描述符,但有时代码直接写入文件描述符3,我猜测(我可以想象一个程序检查一个文件描述符,看看它是否是开放的,如果是的话就使用它,或者如果不是这样的话就会有不同的表现)。 所以后者可能是最好的记住,并用于通用的情况。

必须在管道命令返回后立即将PIPESTATUS [@]复制到数组中。 任何读取PIPESTATUS [@]都会清除内容。 如果您打算检查所有管道命令的状态,请将其复制到另一个阵列。 “$?” 与“$ {PIPESTATUS [@]}”的最后一个元素是相同的值,读取它似乎会破坏“$ {PIPESTATUS [@]}”,但是我没有完全验证这一点。

 declare -a PSA cmd1 | cmd2 | cmd3 PSA=( "${PIPESTATUS[@]}" ) 

如果管道在子外壳中,这将不起作用。 为了解决这个问题,
请参阅bash pipestatus反向命令?

在Ubuntu和Debian中,你可以apt-get install moreutils 。 这包含一个名为mispipe的实用程序,它返回管道中第一个命令的退出状态。

在bash之外,你可以这样做:

 bash -o pipefail -c "command1 | tee output" 

例如,在shell预期为/bin/sh忍者脚本中,这是非常有用的。

在纯bash中最简单的方法是使用流程替换而不是流水线。 有几个区别,但是对于你的用例来说可能并不重要:

  • 当运行一个管道时,bash会等待所有进程完成。
  • 发送Ctrl-C到bash使得它杀死一个管道的所有进程,而不仅仅是主进程。
  • pipefail选项和PIPESTATUS变量与进程替换无关。
  • 可能更多

随着流程的替代,bash只是开始这个​​过程,忘记了这个过程,甚至在jobs看不到。

除了提到的差异, consumer < <(producer)producer | consumer producer | consumer基本上是等同的

如果你想要翻转哪一个是“主要”过程,那么你只需将命令和替换的方向翻转到producer > >(consumer) 。 在你的情况下:

 command > >(tee out.txt) 

例:

 $ { echo "hello world"; false; } > >(tee out.txt) hello world $ echo $? 1 $ cat out.txt hello world $ echo "hello world" > >(tee out.txt) hello world $ echo $? 0 $ cat out.txt hello world 

正如我所说,与管道表达有所不同。 这个过程可能永远不会停止运行,除非它对管道关闭敏感。 特别是,它可能会继续写你的stdout,这可能会令人困惑。

纯壳解决方案:

 % rm -f error.flag; echo hello world \ | (cat || echo "First command failed: $?" >> error.flag) \ | (cat || echo "Second command failed: $?" >> error.flag) \ | (cat || echo "Third command failed: $?" >> error.flag) \ ; test -s error.flag && (echo Some command failed: ; cat error.flag) hello world 

现在用第二cat换成false

 % rm -f error.flag; echo hello world \ | (cat || echo "First command failed: $?" >> error.flag) \ | (false || echo "Second command failed: $?" >> error.flag) \ | (cat || echo "Third command failed: $?" >> error.flag) \ ; test -s error.flag && (echo Some command failed: ; cat error.flag) Some command failed: Second command failed: 1 First command failed: 141 

请注意,第一只猫也失败了,因为它的标准输出关闭了。 日志中的失败命令的顺序在本例中是正确的,但不要依赖它。

这个方法允许为单个命令捕获stdout和stderr,所以如果发生错误,你也可以将其转储到日志文件中,或者如果没有错误(比如dd的输出)就删除它。

基于@布赖恩 – 威尔逊的答案; 这个bash帮助函数:

 pipestatus() { local S=("${PIPESTATUS[@]}") if test -n "$*" then test "$*" = "${S[*]}" else ! [[ "${S[@]}" =~ [^0\ ] ]] fi } 

如此使用:

1:get_bad_things必须成功,但不应该产生输出; 但我们希望看到它产生的输出

 get_bad_things | grep '^' pipeinfo 0 1 || return 

2:所有管道必须成功

 thing | something -q | thingy pipeinfo || return 

有时使用外部命令可能会更简单明了,而不是深入讨论bash的细节。 管道 ,从最小的进程脚本语言execline ,退出与第二个命令*的返回代码,就像sh管道一样,但不像sh ,它允许反转管道的方向,以便我们可以捕获返回代码生产者进程(以下全部在sh命令行上,但安装了execline ):

 $ # using the full execline grammar with the execlineb parser: $ execlineb -c 'pipeline { echo "hello world" } tee out.txt' hello world $ cat out.txt hello world $ # for these simple examples, one can forego the parser and just use "" as a separator $ # traditional order $ pipeline echo "hello world" "" tee out.txt hello world $ # "write" order (second command writes rather than reads) $ pipeline -w tee out.txt "" echo "hello world" hello world $ # pipeline execs into the second command, so that's the RC we get $ pipeline -w tee out.txt "" false; echo $? 1 $ pipeline -w tee out.txt "" true; echo $? 0 $ # output and exit status $ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?" hello world RC: 42 $ cat out.txt hello world 

使用pipeline与本机bash pipeline具有相同的区别,因为在回答#43972501中使用了bash流程替换。

*实际上, pipeline根本不会退出,除非出现错误。 它执行到第二个命令,所以它是第二个返回的命令。

 (command | tee out.txt; exit ${PIPESTATUS[0]}) 

与@CODAR的回答不同,这将返回第一个命令的原始退出码,不仅成功为0,失败为127。 但是@Chaoran指出你可以直接调用${PIPESTATUS[0]} 。 所有内容都放在括号内,这一点很重要。