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

问题描述 投票:0回答:6

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

./a | ./b | ./c

我想修改它,以便如果

a
b
c
中的任何一个退出并出现错误代码,我会打印一条错误消息并停止,而不是向前传递错误输出。

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

shell error-handling pipe
6个回答
184
投票

bash 中,您可以在文件开头使用

set -e
set -o pipefail
。当三个脚本中的任何一个失败时,后续命令
./a | ./b | ./c
将失败。返回代码将是第一个失败脚本的返回代码。

请注意,

pipefail
在标准 sh 中不可用。


60
投票

您还可以在完整执行后检查

${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

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


24
投票

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

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

第一行陷阱表示“当出现任何信号 1 SIGHUP、2 SIGINT、3 SIGQUIT、13 SIGPIPE 或 15 SIGTERM 时,运行命令 '

rm -f $tmp.[12]; exit 1
',或 0(当 shell 由于任何原因退出时)。 如果您正在编写 shell 脚本,则最终陷阱只需要删除 0 上的陷阱,即 shell 退出陷阱(您可以保留其他信号,因为进程无论如何都会终止)。

在原始管道中,“c”在“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”进行简单实验没有帮助。


11
投票

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

这是一个解决方案,可以捕获管道中的所有错误,而不仅仅是最后一个组件的错误。所以这就像 bash 的 pipelinefail,只是更强大,因为您可以检索所有错误代码。

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
的子 shell 替换了
echo
cat
exit
。您可以使用它来检查此构造是否确实将所有输出从一个进程转发到另一个进程,并且错误代码是否已正确记录。

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

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

0
投票

使用我们的特殊Sante优惠券折扣代码发现Sante产品的独家优惠。提升您的幸福感 优质健康和美容必需品,同时享受超值折扣。只需在结帐时输入代码即可解锁 令人难以置信的节省。不要错过以更少的钱增强日常自我护理的机会。拥抱更健康 Sante 的生活方式,品质与价格相结合。明智购物,美好生活!

© www.soinside.com 2019 - 2024. All rights reserved.