我目前有一个脚本,其功能类似于
./a | ./b | ./c
我想对其进行修改,以便如果a,b或c中的任何一个退出并显示错误代码,我都会打印一条错误消息并停止,而不是向前传递错误的输出。
最简单/最干净的方法是什么?
a
,b
,c
的命令不是顺序而是并行运行。换句话说,数据顺序地从流入a
到c
,但实际a
,b
并且c
命令开始(大致)同时。
我目前有一个脚本,其功能类似于
./a | ./b | ./c
我想对其进行修改,以便如果a,b或c中的任何一个退出并显示错误代码,我都会打印一条错误消息并停止,而不是向前传递错误的输出。
最简单/最干净的方法是什么?
a
,b
,c
的命令不是顺序而是并行运行。换句话说,数据顺序地从流入a
到c
,但实际a
,b
并且c
命令开始(大致)同时。
Answers:
如果您确实不希望第二个命令在知道第一个命令成功之前继续执行,那么您可能需要使用临时文件。它的简单版本是:
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'进行简单的实验没有帮助。
mktemp
或tempfile
。
trap
请记住,用户始终可以发送SIGKILL
到进程以立即终止该进程,在这种情况下,陷阱将不起作用。当系统停电时也是如此。创建临时文件时,请确保使用mktemp
该文件,因为这会将您的文件放在重新启动后通常会清理到的位置/tmp
。
在bash中,您可以使用set -e
和set -o pipefail
在文件的开头。./a | ./b | ./c
当三个脚本中的任何一个失败时,后续命令将失败。该返回码将是第一个失败脚本的返回码。
请注意,这pipefail
在标准sh中不可用。
set
(总共4个不同的版本),看看这如何影响最后一个的输出echo
。
您也可以${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
将允许您检查管道中的所有命令是否成功。
#!/bin/sh
,因为如果sh
不是bash,它将无法正常工作。(通过记住使用,可以轻松修复#!/bin/bash
。)
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'
-如果$PIPESTATUS[@]
仅包含0和空格(如果管道中的所有命令均成功),则返回0 。
command1 && command2 | command3
如果其中任何一个失败,则您的解决方案将返回非零值。
不幸的是,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 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
&&|
,这意味着“仅在前面的命令成功的情况下才继续管道”。我想您可能也有|||
这样的意思,即“如果前面的命令失败,则继续执行管道操作”(并可能通过管道传递Bash 4的错误消息|&
)。