忽略Bash pipefail中的错误代码141
在bash中设置pipefail
选项(通过set -o pipefail
)可以让脚本在管道的任何一步出现非零错误时失败,也就是说,如果有错误发生,脚本会停止运行。
不过,我们遇到了SIGPIPE
错误(错误代码141),这是因为往一个已经不存在的管道写数据。
有没有办法让bash忽略SIGPIPE
错误,或者有没有什么方法可以写一个错误处理程序,处理所有的错误状态码,但不包括0和141呢?
比如,在Python中,我们可以添加:
signal.signal(signal.SIGPIPE, signal.SIG_DFL)
来让SIGPIPE
错误的默认行为是被忽略(参考http://coding.derkeiler.com/Archive/Python/comp.lang.python/2004-06/3823.html)。
在bash中有没有类似的选项呢?
5 个回答
set -o errexit # Exit on error, do not continue running the script
set -o nounset # Trying to access a variable that has not been set generates an error
set -o pipefail # When a pipe fails generate an error
random="$(!(cat /dev/urandom) | tr -dc A-Za-z0-9 | head -c 10)"
echo $random
因为我们把'cat'给去掉了,所以会导致管道失败。但是通过用'!'来否定状态,就不会触发管道失败的检测了。
有没有办法让bash忽略SIGPIPE错误,或者有没有什么方法可以写一个错误处理程序,处理所有错误状态码,但不包括0和141呢?
对于第二个问题的另一种解决方案是,忽略来自子进程的SIGPIPE终止信号(退出码141):
cmd1 | cmd2 | cmd3 || { ec=$?; [ $ec -eq 141 ] && true || (exit $ec); }
这个方法使用了exit
命令在一个子shell中,这样可以保留命令管道的原始退出码,前提是它不是141。因此,如果同时使用了set -e
(set -o errexit
)和set -o pipefail
,它将达到预期效果。
我们可以使用一个函数来让代码更简洁,这样可以使用return
,而不是把exit
放在子shell中的技巧:
handle_pipefails() {
# ignore exit code 141 from command pipes
[ $1 -eq 141 ] && return 0
return $1
}
# then use it or test it as:
yes | head -n 1 || handle_pipefails $?
echo "ec=$?"
# then change the tested code from 141 to e.g. 999 in
# the function, and see that ec was in fact captured as
# 141, unlike the current highest voted answer which
# exits with code 1.
# An alternative, if you want to test the exit status of all commands in a pipe:
handle_pipefails2() {
# ignore exit code 141 from more complex command pipes
# - use with: cmd1 | cmd2 | cmd3 || handle_pipefails2 "${PIPESTATUS[@]}"
for x in "$@"; do
(( $x == 141 )) || { (( $x > 0 )) && return $x; }
done
return 0
}
附注:问题解读
正如在@chepner的回答评论中指出的,理解问题的第一部分更困难——一种理解是“忽略由SIGPIPE产生的子进程的错误代码”,而上面的代码正是实现了这一点。然而,让Bash完全忽略SIGPIPE 信号可能会导致子进程一直写入,因为它永远不会收到终止信号。例如:
(yes | head -n 1; echo $?)
# y
# 141
(trap '' PIPE; yes | head -n 1; echo $?)
# must be terminated with Ctrl-C, as `yes` will write forever
我不知道有没有办法可以对整个脚本这样做。这样做一般来说是有风险的,因为我们无法确定子进程返回141的原因是否不同。
不过,你可以对每个命令单独处理。||
这个符号可以屏蔽第一个命令返回的任何错误,所以你可以这样做:
set -e -o pipefail
(cat /dev/urandom || true) | head -c 10 | base64
echo 'cat exited with SIGPIPE, but we still got here!'
我在每个处理流程中都这样做:我加上一个 || if ...
的语句,这样可以忽略退出代码141,但对于其他任何错误则会生成退出代码1。(导致非141失败的原始退出代码会丢失,因为在 if
之后,$?
的值被测试改变了。)
pipe | that | fails || if [[ $? -eq 141 ]]; then true; else exit $?; fi
trap
命令可以让你指定在遇到某个信号时要执行的命令。如果你想忽略这个信号,可以传递一个空字符串:
trap '' PIPE