Pipe output and capture exit status in Bash
我想在bash中执行一个长时间运行的命令,并捕获它的退出状态,并测试它的输出。
所以我这样做:
1 2 | command | tee out.txt ST=$? |
问题在于,变量st捕获
请注意,该命令运行时间很长,将输出重定向到文件以供以后查看对我来说不是一个好的解决方案。
有一个名为
1 | <command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0 |
或者另一个同样适用于其他外壳(如zsh)的替代方案是启用pipefail:
1 2 | set -o pipefail ... |
由于语法稍有不同,第一个选项不适用于
使用bash的
pipefail: the return value of a pipeline is the status of
the last command to exit with a non-zero status,
or zero if no command exited with a non-zero status
哑解决方案:通过命名管道(mkfifo)连接它们。然后可以再次运行该命令。
1 2 3 4 | mkfifo pipe tee out.txt < pipe & command > pipe echo $? |
有一个数组为您提供管道中每个命令的退出状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ 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特定功能或临时文件的情况下工作。另外:最后,退出状态实际上是一个退出状态,而不是文件中的某个字符串。
情况:
1 | someprog | filter |
您需要
这是我的解决方案:
1 2 3 | ((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1 echo $? |
请参阅我在unix.stackexchange.com上对相同问题的回答,以获得详细的解释和没有子shell和一些警告的替代方案。
通过将
下面是一个例子:
1 2 3 | # the"false" shell built-in command returns 1 false | tee ; ( exit ${PIPESTATUS[0]} ) echo"return value: $?" |
会给你:
所以我想提出一个像莱斯马纳那样的答案,但我认为我的答案可能更简单一些,更有利一些,纯粹的Bourne-Shell解决方案:好的。
1 2 3 4 | # 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中-因为这就是命令替换(backticks)将捕获的内容,而且s什么将被放入变量中。好的。
最后一点神奇的是,第一个
(
你可以用一种不那么技术性和更好玩的方式来看待它,就好像命令的输出是相互跳跃的:command1将管道连接到command2,然后printf的输出跳过command2,这样command2就不会捕获它,然后command2的输出跳过并跳出命令替换,就像printf及时到达以获取由替换捕获,以便它最终进入变量,command2的输出以其愉快的方式写入标准输出,就像在普通管道中一样。好的。
另外,据我所知,
根据Lesmana提到的警告,command1可能在某个时候会使用文件描述符3或4,所以为了更健壮,您可以这样做:好的。
1 2 3 | exec 4>&1 exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1` exec 4>&- |
注意,在我的示例中,我使用复合命令,但是子shell(使用
命令从启动它们的进程继承文件描述符,因此整个第二行将继承文件描述符4,后面跟着
我不确定文件描述符3和4直接使用的频率-我认为大多数时候程序使用的是系统调用,它们返回的不是当前使用的文件描述符,但有时代码直接写入文件描述符3,我猜(我可以想象一个程序检查文件描述符,看它是否打开,如果打开,使用它,或者behavin如果不是的话,那就不一样了。因此,后者可能最好记住并用于一般情况。好的。好啊。
在Ubuntu和Debian中,您可以使用ecx1(9)。它包含一个名为
1 | (command | tee out.txt; exit ${PIPESTATUS[0]}) |
与@codar的答案不同,这将返回第一个命令的原始退出代码,成功时不仅返回0,失败时返回127。但正如@chauran所指出的,你可以直接打电话给
pipestatus[@]必须在pipe命令返回后立即复制到数组。任何对pipestatus[@]的读取都将擦除内容。如果计划检查所有管道命令的状态,请将其复制到另一个数组。"$?"与"$PipeStatus[@]"的最后一个元素的值相同,阅读它似乎会破坏"$pipestatus[@]",但我还没有完全验证这一点。
1 2 3 | declare -a PSA cmd1 | cmd2 | cmd3 PSA=("${PIPESTATUS[@]}" ) |
如果管道在子壳中,这将不起作用。为了解决这个问题,在backticked命令中看到bash pipestatus吗?
在普通bash中实现这一点的最简单方法是使用流程替换而不是管道。有几个不同之处,但对于您的用例来说,它们可能并不重要:
- 运行管道时,bash会等待所有进程完成。
- 将ctrl-c发送到bash会杀死管道的所有进程,而不仅仅是主进程。
pipefail 选项和PIPESTATUS 变量与过程替换无关。- 可能更多
通过过程替换,bash只是启动了过程并忽略了它,它甚至在
除上述差异外,
如果要翻转哪个是"主"进程,只需将命令和替换方向翻转到
1 | command > >(tee out.txt) |
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ { 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 |
正如我所说,管道表达式有区别。除非该过程对管道关闭敏感,否则它可能永远不会停止运行。尤其是,它可能会一直在给你的性传播疾病写东西,这可能会让人困惑。
在bash之外,您可以执行以下操作:
1 | bash -o pipefail -c"command1 | tee output" |
这在忍者脚本中很有用,其中shell应该是
有时使用外部命令可能更简单、更清晰,而不是深入挖掘bash的细节。pipe line,从最小进程脚本语言execline,以第二个命令*的返回代码退出,就像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | $ # 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 |
使用
*实际上,除非出现错误,否则
基于@brian-s-wilson的答案;此bash helper函数:
1 2 3 4 5 6 7 8 | pipestatus() { local S=("${PIPESTATUS[@]}") if test -n"$*" then test"$*" ="${S[*]}" else ! [["${S[@]}" =~ [^0\ ] ]] fi } |
因此使用:
1:不好的事情必须成功,但它不应该产生输出;但我们希望看到它确实产生的输出
1 2 | get_bad_things | grep '^' pipeinfo 0 1 || return |
2:所有管道必须成功
1 2 | thing | something -q | thingy pipeinfo || return |
纯壳液:
1 2 3 4 5 6 | % 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 |
现在第二个
1 2 3 4 5 6 7 8 | % 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会关闭。在本例中,日志中失败命令的顺序是正确的,但不要依赖于它。
此方法允许为单个命令捕获stdout和stderr,这样您就可以在发生错误时将其转储到日志文件中,或者在没有错误时将其删除(如dd的输出)。