如何在 shell 管道中捕获错误代码?

我现在有一个脚本

./a | ./b | ./c

我想修改它,以便如果任何 ab,或 c退出与错误代码我打印一个错误消息和停止,而不是管道向前错误输出。

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

48765 次浏览

如果您确实不希望在第一个命令成功之前继续执行第二个命令,那么您可能需要使用临时文件。简单来说就是:

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”重定向也可以缩写为“ > & 2”; 然而,MKS shell 的一个旧版本在没有前面的“1”的情况下错误地处理了错误重定向,所以我多年来一直使用这种明确的符号来表示可靠性。

如果你中断了某些事情,这会泄漏文件。防爆(或多或少) 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

The first trap line says 'run the commands 'rm -f $tmp.[12]; exit 1' when any of the signals 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE, or 15 SIGTERM occur, or 0 (when the shell exits for any reason). 如果您正在编写一个 shell 脚本,那么最终的陷阱只需要删除0上的陷阱,这是 shell 退出陷阱(您可以保留其他信号,因为进程无论如何都要终止)。

在最初的流水线中,“ c”在“ a”完成之前从“ b”读取数据是可行的——这通常是可取的(例如,它给多个核心工作)。如果‘ b’是一个‘ sort’阶段,那么这就不适用了——‘ b’在生成任何输出之前必须看到它的所有输入。

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

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

这是简单和对称的-它是平凡的扩展到4部分或 N 部分管道。

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

In bash you can use set -e and set -o pipefail at the beginning of your file. A subsequent command ./a | ./b | ./c will fail when any of the three scripts fails. The return code will be the return code of the first failed script.

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

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

./a | ./b | ./c

Then ${PIPESTATUS} will be an array of error codes from each command in the pipe, so if the middle command failed, echo ${PIPESTATUS[@]} would contain something like:

0 1 0

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

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

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

不幸的是,Johnathan 的答案需要临时文件,Michel 和 Imron 的答案需要 bash (尽管这个问题被标记为 shell)。正如其他人已经指出的那样,在以后的进程启动之前不可能中止管道。所有进程都会立即启动,因此所有进程都会在任何错误传达之前运行。但是这个问题的题目也是关于错误代码的。可以在管道完成后检索和调查这些流程,以确定是否有任何相关流程失败。

下面的解决方案可以捕获管道中的所有错误,而不仅仅是最后一个组件的错误。所以这就像 bash 的 pipefault 一样,只是在检索 all错误代码方面更加强大。

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中,并在以后进行调查。这也是为什么所有进程的标准错误被重定向到标准输出。您也可以将输出发送到 /dev/null,或者将其作为出错的另一个指示器。如果您不需要将最后一个命令的输出存储在任何地方,那么您可以用一个文件替换最后一个重定向到 /dev/null的命令。

为了更好地使用这个结构,并使自己相信它确实做了应该做的事情,我用执行 echocatexit的子 shell 替换了 ./a./b./c。您可以使用它来检查此构造是否真正将一个进程的所有输出转发到另一个进程,以及是否正确地记录了错误代码。

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

This answer is in the spirit of the accepted answer, but using shell variables instead of temporary files.

if TMP_A="$(./a)"
then
if TMP_B="$(echo "TMP_A" | ./b)"
then
if TMP_C="$(echo "TMP_B" | ./c)"
then
echo "$TMP_C"
else
echo "./c failed"
fi
else
echo "./b failed"
fi
else
echo "./a failed"
fi