在 shell 管道中捕获错误代码

Posted

技术标签:

【中文标题】在 shell 管道中捕获错误代码【英文标题】:Catching error codes in a shell pipe 【发布时间】:2010-12-05 18:36:06 【问题描述】:

我目前有一个类似的脚本

./a | ./b | ./c

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

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

【问题讨论】:

确实需要像&&| 这样的东西,这意味着“只有在前面的命令成功时才继续管道”。我想您也可以拥有|||,这意味着“如果前面的命令失败,则继续管道”(并且可能会像 Bash 4 的 |& 一样通过管道传输错误消息)。 @DennisWilliamson,您不能“停止管道”,因为abc 命令不是按顺序运行而是并行运行。换句话说,数据按顺序从a 流向c,但实际的abc 命令(大致)同时开始。 【参考方案1】:

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

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

【讨论】:

我不知道 pipefail,真的很方便。 它旨在将其放入脚本中,而不是在交互式外壳中。您的行为可以简化为 set -e;错误的;它还退出 shell 进程,这是预期的行为;) 注意:这仍然会执行所有三个脚本,并且不会在第一个错误时停止管道。 @n2liquid-GuilhermeVieira 顺便说一句,“不同的变体”,我的具体意思是,删除 set 中的一个或两个(总共 4 个不同的版本),看看它如何影响输出最后echo. @josch 我在从谷歌搜索如何在 bash 中执行此操作时找到了此页面,但“这正是我正在寻找的”。我怀疑许多支持答案的人都经历了类似的思考过程,并且不检查标签和标签的定义。【参考方案2】:

您还可以在完整执行后检查$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

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

【讨论】:

这是 bashish --- 它是一个 bash 扩展而不是 Posix 标准的一部分,因此其他 shell,如 dash 和 ash 将不支持它。这意味着如果您尝试在以#!/bin/sh 开头的脚本中使用它,您可能会遇到麻烦,因为如果sh 不是 bash,它将无法工作。 (记住改用#!/bin/bash 可以轻松解决。) echo $PIPESTATUS[@] | grep -qE '^[0 ]+$' - 如果$PIPESTATUS[@] 仅包含 0 和空格(如果管道中的所有命令都成功),则返回 0。 @MattBianco 这对我来说是最好的解决方案。它也适用于 && 例如command1 && command2 | command3 如果其中任何一个失败,您的解决方案将返回非零值。【参考方案3】:

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

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 或 0(当 shell 出于任何原因退出时)时,第一个陷阱行显示“运行命令 'rm -f $tmp.[12]; exit 1' . 如果你写的是shell脚本,最后的trap只需要去掉0上的trap,也就是shell退出trap(其他信号你可以留着,反正进程即将终止)。

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

【讨论】:

我建议使用mktemptempfile @Dennis:是的,我想我应该习惯诸如 mktemp 或 tmpfile 之类的命令;哦,这么多年前,当我学习它时,它们并不存在于 shell 级别。让我们快速检查一下。我在 MacOS X 上找到 mktemp;我在 Solaris 上有 mktemp,但这只是因为我安装了 GNU 工具;似乎 mktemp 出现在古董 HP-UX 上。我不确定是否存在跨平台的 mktemp 通用调用。 POSIX 既不标准化 mktemp 也不标准化 tmpfile。我没有在我可以访问的平台上找到 tmpfile。因此,我将无法在可移植 shell 脚本中使用这些命令。 在使用trap 时,请记住用户可以随时向进程发送SIGKILL 以立即终止它,在这种情况下您的陷阱不会生效。当您的系统停电时也是如此。创建临时文件时,请确保使用mktemp,因为这会将您的文件放在某个地方,重新启动后它们会被清理(通常是/tmp)。【参考方案4】:

不幸的是,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 的文件替换为文件。

为了更多地使用这个构造并说服自己这确实做了它应该做的,我用执行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

【讨论】:

【参考方案5】:

此答案本着公认答案的精神,但使用 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

【讨论】:

以上是关于在 shell 管道中捕获错误代码的主要内容,如果未能解决你的问题,请参考以下文章

在可管道 rxjs 运算符的组合管道中捕获错误

如何对这个从管道可观察到的错误捕获的 Angular 打字稿 Http 错误拦截器进行单元测试?

在 Ruby exec 语句中使用命名管道

如何在 Shell 脚本中建立远程连接时捕获错误

在 Python 2 和 Python 3 中捕获断管

Close-SMBOpenFile引发错误,没有被try-catch捕获