捕获shell管道中的错误代码

hug*_*omg 91 error-handling shell pipe

我目前有一个类似的脚本

./a | ./b | ./c
Run Code Online (Sandbox Code Playgroud)

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

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

Mic*_*mia 144

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

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

  • 注意:这仍将执行所有三个脚本,并且不会在第一次出错时停止管道. (10认同)

Imr*_*ron 40

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

./a | ./b | ./c
Run Code Online (Sandbox Code Playgroud)

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

0 1 0
Run Code Online (Sandbox Code Playgroud)

以及类似的东西在命令之后运行:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0
Run Code Online (Sandbox Code Playgroud)

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

  • 这是bashish ---它是一个bash扩展而不是Posix标准的一部分,因此其他像dash和ash这样的shell将不支持它.这意味着如果你尝试在启动`#!/ bin/sh`的脚本中使用它会遇到麻烦,因为如果`sh`不是bash,它将无效.(通过记住使用`#!/ bin/bash`来轻松修复.) (10认同)
  • `echo $ {PIPESTATUS [@]} | grep -qE'^ [0] + $'-如果$ PIPESTATUS [@]仅包含0和空格(如果管道中的所有命令均成功),则返回0。 (2认同)

Jon*_*ler 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]
Run Code Online (Sandbox Code Playgroud)

'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
Run Code Online (Sandbox Code Playgroud)

rm -f $tmp.[12]; exit 1当任何信号发生1 SIGHUP,2 SIGINT,3 SIGQUIT,13 SIGPIPE或15 SIGTERM时,第一个陷阱线表示"运行命令" ,或者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)
Run Code Online (Sandbox Code Playgroud)

这很简单且对称 - 扩展到4部分或N部分管道是微不足道的.

使用'set -e'进行简单的实验并没有帮助.

  • 我建议使用“mktemp”或“tempfile”。 (2认同)

jos*_*sch 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
Run Code Online (Sandbox Code Playgroud)

要检测是否有任何失败,echo在任何命令失败的情况下,命令会打印标准错误.然后将组合的标准错误输出保存$res并稍后进行调查.这也是为什么所有进程的标准错误被重定向到标准输出的原因.您也可以将该输出发送到/dev/null或作为另一个指示出错的指示./dev/null如果你没有将最后一个命令的输出存储在任何地方,你可以用文件替换最后一个重定向.

为了更多地使用这个结构,并说服自己这确实做了它应该做的事情,我替换了./a,./b并且./c通过子shell执行echo,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
Run Code Online (Sandbox Code Playgroud)