捕获shell管道中的错误代码

时间:2009-10-11 15:17:57

标签: shell error-handling pipe

我目前有一个类似

的脚本
./a | ./b | ./c

我想修改它,以便如果a,b或c中的任何一个退出并带有错误代码我会打印错误消息并停止而不是向前输出错误的输出。

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

5 个答案:

答案 0 :(得分:138)

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

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

答案 1 :(得分:38)

您还可以在完整执行后检查${PIPESTATUS[]}数组,例如如果你跑:

./a | ./b | ./c

然后${PIPESTATUS}将是管道中每个命令的错误代码数组,因此如果中间命令失败,echo ${PIPESTATUS[@]}将包含如下内容:

0 1 0

之类的命令在命令之后运行:

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

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

答案 2 :(得分:19)

如果您确实不希望第二个命令继续进行,直到第一个命令成功,那么您可能需要使用临时文件。简单的版本是:

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&gt;&amp; 2'重定向也可以缩写为'&gt;&amp; 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

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

在原始流水线中,'a'在'a'完成之前从'b'读取数据是可行的 - 这通常是可取的(例如,它允许多个核工作)。如果'b'是'排序'阶段,那么这将不适用 - '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'进行简单的实验并没有帮助。

答案 3 :(得分:8)

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

这是一个捕获管道中所有错误的解决方案,而不仅仅是最后一个组件的错误。所以这就像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中并稍后进行调查。这也是为什么所有进程的标准错误被重定向到标准输出的原因。您也可以将该输出发送到/dev/null,或者将其作为另一个出错的指示。如果你没有将最后一个命令的输出存储在任何地方,你可以用文件将最后一次重定向替换为/dev/null

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

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

答案 4 :(得分:1)

这个答案符合公认答案的精神,但使用的是 shell 变量而不是临时文件。

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