在壳管中捕获错误代码


97

我目前有一个脚本,其功能类似于

./a | ./b | ./c

我想对其进行修改,以便如果a,b或c中的任何一个退出并显示错误代码,我都会打印一条错误消息并停止,而不是向前传递错误的输出。

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


7
确实需要某种类似的东西&&|,这意味着“仅在前面的命令成功的情况下才继续管道”。我想您可能也有|||这样的意思,即“如果前面的命令失败,则继续执行管道操作”(并可能通过管道传递Bash 4的错误消息|&)。
暂停,直到另行通知。

6
@DennisWilliamson,你不能“阻止管道”,因为abc的命令不是顺序而是并行运行。换句话说,数据顺序地从流入ac,但实际ab并且c命令开始(大致)同时。
Giacomo 2012年

Answers:


20

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

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

rm -f $tmp.[12]; exit 1当出现以下任何一个信号1 SIGHUP,2 SIGINT,3 SIGQUIT,13 SIGPIPE或15 SIGTERM或0(当外壳出于任何原因退出时)时,第一条陷阱行都说“运行命令” 。如果您正在编写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'进行简单的实验没有帮助。


1
我建议使用mktemptempfile
暂停,直到另行通知。

@丹尼斯:是的,我想我应该习惯于诸如mktemp或tmpfile之类的命令;当我了解它的时候,它们在shell级别还不存在。让我们快速检查一下。我在MacOS X上找到了mktemp;我在Solaris上具有mktemp,但这仅是因为我已经安装了GNU工具。看来老式HP-UX上存在mktemp。我不确定是否有跨平台通用的mktemp调用。POSIX既没有标准化mktemp也没有标准化tmpfile。我在可以访问的平台上找不到tmpfile。因此,我将无法在可移植的shell脚本中使用这些命令。
乔纳森·莱夫勒

1
使用时trap请记住,用户始终可以发送SIGKILL到进程以立即终止该进程,在这种情况下,陷阱将不起作用。当系统停电时也是如此。创建临时文件时,请确保使用mktemp该文件,因为这会将您的文件放在重新启动后通常会清理到的位置/tmp
josch

155

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

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


我不知道pipefail,真的很方便。
菲尔·杰克逊

它旨在将其放入脚本中,而不是放在交互式外壳中。您的行为可以简化为set -e; 假; 它还退出了shell进程,这是预期的行为;)
Michel Samia 2013年

11
注意:这仍将执行所有三个脚本,并且不会在出现第一个错误时停止管道。
海德

1
@ n2liquid-GuilhermeVieira顺便说一句,我的意思是“不同的变体”,删除其中的一个或两个set(总共4个不同的版本),看看这如何影响最后一个的输出echo
海德2014年

1
@josch我在搜索如何从Google以bash进行搜索时找到了此页面,尽管,“这正是我想要的”。我怀疑许多赞成答案的人都会经历类似的思考过程,而不会检查标签和标签的定义。
Troy Daniels

43

您也可以${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

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


11
这是bashish ---这是bash扩展,不是Posix标准的一部分,因此dash和ash等其他shell将不支持它。这意味着,如果您尝试在start脚本中使用它,可能会遇到麻烦#!/bin/sh,因为如果sh不是bash,它将无法正常工作。(通过记住使用,可以轻松修复#!/bin/bash。)
David Given

2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'-如果$PIPESTATUS[@]仅包含0和空格(如果管道中的所有命令均成功),则返回0 。
MattBianco

@MattBianco这对我来说是最好的解决方案。它还与&&一起使用,例如,command1 && command2 | command3如果其中任何一个失败,则您的解决方案将返回非零值。
DavidC

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通过执行子shell echocatexit。您可以使用它来检查此构造是否确实将所有输出从一个进程转发到另一个进程,以及是否正确记录了错误代码。

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.