忽略Bash pipefail中的错误代码141

30 投票
5 回答
14355 浏览
提问于 2025-04-17 22:43

在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 个回答

0
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'给去掉了,所以会导致管道失败。但是通过用'!'来否定状态,就不会触发管道失败的检测了。

4

有没有办法让bash忽略SIGPIPE错误,或者有没有什么方法可以写一个错误处理程序,处理所有错误状态码,但不包括0和141呢?

对于第二个问题的另一种解决方案是,忽略来自子进程的SIGPIPE终止信号(退出码141):

cmd1 | cmd2 | cmd3 || { ec=$?; [ $ec -eq 141 ] && true || (exit $ec); }

这个方法使用了exit命令在一个子shell中,这样可以保留命令管道的原始退出码,前提是它不是141。因此,如果同时使用了set -eset -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
7

我不知道有没有办法可以对整个脚本这样做。这样做一般来说是有风险的,因为我们无法确定子进程返回141的原因是否不同。

不过,你可以对每个命令单独处理。||这个符号可以屏蔽第一个命令返回的任何错误,所以你可以这样做:

set -e -o pipefail
(cat /dev/urandom || true) | head -c 10 | base64
echo 'cat exited with SIGPIPE, but we still got here!'
25

我在每个处理流程中都这样做:我加上一个 || if ... 的语句,这样可以忽略退出代码141,但对于其他任何错误则会生成退出代码1。(导致非141失败的原始退出代码会丢失,因为在 if 之后,$? 的值被测试改变了。)

pipe | that | fails || if [[ $? -eq 141 ]]; then true; else exit $?; fi
11

trap 命令可以让你指定在遇到某个信号时要执行的命令。如果你想忽略这个信号,可以传递一个空字符串:

trap '' PIPE

撰写回答