在shellpipe道中捕获错误代码

我目前有一个脚本,可以做类似的事情

./a | ./b | ./c 

我想修改它,如果任何a,b或c退出并显示错误代码,我会打印一条错误消息并停止,而不是向前输送错误的输出。

最简单/最干净的方法是什么?

如果你真的不想第二个命令继续下去,直到第一个命令成功,那么你可能需要使用临时文件。 简单的版本是:

 tmp=${TMPDIR:-/tmp}/mine.$$ if ./a > $tmp.1 then if ./b <$tmp.1 >$tmp.2 then if ./c <$tmp.2 then : OK else echo "./c failed" 1>&2 fi else echo "./b failed" 1>&2 fi else echo "./a failed" 1>&2 fi rm -f $tmp.[12] 

'1&2'redirect也可以缩写为'&2'; 然而,旧版本的MKS shell错误地处理了错误redirect而没有前面的'1',所以我已经使用了这个明确的符号表示可靠性的年龄。

如果你打断某些东西,这会泄漏文件。 防弹(或多或less)的shell编程使用:

 tmp=${TMPDIR:-/tmp}/mine.$$ trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15 ...if statement as before... rm -f $tmp.[12] trap 0 1 2 3 13 15 

第一个陷阱行说'运行命令' rm -f $tmp.[12]; exit 1 当出现任何信号1 SIGHUP,2 SIGINT,3 SIGQUIT,13 SIGPIPE或15 SIGTERM或0(shell因任何原因退出)时退出1。 如果你正在编写一个shell脚本,那么最后一个陷阱只需要去掉shell的退出陷阱(你可以保留其他信号,因为这个过程即将终止)。

在原来的stream水线中,'c'在'a'完成之前从'b'读取数据是可行的 – 这通常是可取的(例如,它可以让多个内核工作)。 如果'b'是'sorting'阶段,那么这将不适用 – 'b'必须先看到它的所有input,然后才能产生任何输出。

如果要检测哪个命令失败,可以使用:

 (./a || echo "./a exited with $?" 1>&2) | (./b || echo "./b exited with $?" 1>&2) | (./c || echo "./c exited with $?" 1>&2) 

这是简单和对称的 – 扩展到4部分或N部分stream水线是微不足道的。

简单的'set -e'实验并没有帮助。

bash中,你可以使用set -eset -o pipefail在你的文件的开头。 后续的命令./a | ./b | ./c ./a | ./b | ./c 如果三个脚本中的任何一个失败,./ ./a | ./b | ./c将会失败。 返回码将是第一个失败脚本的返回码。

请注意, pipefail在标准sh中不可用。

完全执行后,您还可以检查${PIPESTATUS[]}数组,例如,如果运行:

 ./a | ./b | ./c 

那么${PIPESTATUS}将是pipe道中每个命令的错误代码数组,所以如果中间命令失败,那么echo ${PIPESTATUS[@]}将包含如下内容:

 0 1 0 

像这样的东西在命令之后运行:

 test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0 

将允许您检查pipe道中的所有命令是否成功。

不幸的是,Johnathan的答案需要临时文件,Michel和Imron的答案需要bash(即使这个问题是标签shell)。 正如其他人已经指出的,在后面的过程开始之前不可能中止pipe道。 所有的进程都立即开始,并将在任何错误传递之前运行。 但问题的标题也是关于错误代码的问题。 这些可以在pipe道完成后找回并调查,以确定是否有任何涉及的进程失败。

这是一个解决scheme,可以捕获pipe道中的所有错误,而不仅仅是最后一个组件的错误。 所以这就像bash的pipefail,只是更强大的一个意思,你可以检索所有的错误代码。

 res=$( (./a 2>&1 || echo "1st failed with $?" >&2) | (./b 2>&1 || echo "2nd failed with $?" >&2) | (./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1) if [ -n "$res" ]; then echo pipe failed fi 

要检测是否有任何失败,在任何命令失败的情况下, echo命令将打印标准错误。 然后将组合的标准错误输出保存在$res ,稍后调查。 这也是为什么所有进程的标准错误都被redirect到标准输出的原因。 你也可以把这个输出发送到/dev/null或者把它作为另外一个指示器出错。 你可以用一个文件replace最后一个redirect到/dev/null ,如果你没有find最后一个命令的输出。

为了更多地使用这个构造,并且让自己相信这确实是它应该做的,我用执行echocatexit子shellreplace了./b./c 。 你可以用它来检查这个结构是否真正将所有输出从一个进程转发到另一个进程,并且错误代码被正确logging。

 res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) | (sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) | (sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1) if [ -n "$res" ]; then echo pipe failed fi