set -euo pipefail 为什么没救下你的脚本:五个静默失效的场合
给脚本加上 set -euo pipefail,几乎成了写 shell 的第一条纪律。但这条纪律的覆盖面比大多数人以为的窄得多:加上它之后脚本照样能静默跑完、产出一个半截结果,退出码还是 0。
下面五个场合是我在实际脚本里踩过的——每一个都能让 set -e 完全失效,且不报任何错。
一、set -e 在「条件位置」会被整体关闭,并传染给函数内部
set -euo pipefail
check_disk() {
df -h /data # 假设这里失败
echo "checked" # 照样会执行
}
if check_disk; then
echo ok
fi
set -e 的规则是:命令处于 if / while / until 的条件位置、&&/|| 的非末尾位置、或者前面有 ! 时,不触发退出。关键在于这条豁免会继承给函数体内的所有命令——只要这个函数是被 if 调用的,它内部每一个命令都失去了 set -e 保护。
! 更隐蔽:
! grep -q pattern file # grep 失败在这里是"成功",不会退出
修法只有两条:需要保护的步骤自己在函数里 cmd || return 1;或者把函数返回值显式判断,别指望 set -e 兜底。
二、local x=$(cmd) 的退出码不是 cmd 的
set -e
f() {
local out=$(grep -c foo /nonexistent) # grep 失败,但不退出
echo "out=$out"
}
f
local、export、declare 这类内建命令自己总会返回 0,$(...) 的失败被它吞掉了。正确写法是拆成两行:
local out
out=$(grep -c foo /nonexistent)
三、((i++)) 在 i 为 0 时返回 1
set -e
i=0
((i++)) # 表达式取的是自增前的值 0,算术结果为假 → 返回 1 → 脚本退出
echo "不会执行"
这是 set -e 最出名的一个坑,因为循环计数器从 0 开始时必踩。用 i=$((i+1)) 或前置自增 ((++i))。
四、命令替换里的 set -e 不生效
set -e
x=$(false; echo hello)
echo "$x" # 输出 hello,脚本正常结束
按 POSIX,命令替换产生的子 shell 会忽略 set -e。bash 4.4 起提供了 shopt -s inherit_errexit 让子 shell 继承,但老机器上默认没有。稳妥做法是让命令替换里的最后一条命令自己返回失败,或者干脆别把多条命令塞进 $( )。
五、pipefail 与 SIGPIPE 的误伤
set -o pipefail 解决的是 tar czf - dir | gzip > x.tgz 里前半段失败被忽略的问题,这点很有用。但它也会让「正常提前退出」的下游把上游判成失败:
set -euo pipefail
seq 1 1000000 | head -3 # 退出码 141(SIGPIPE),整个管道失败
head、grep -q、awk 'exit' 都会提前关闭管道,上游收到 SIGPIPE 返回 141。这类命令要么允许失败(|| [ $? -eq 141 ]),要么用 ${PIPESTATUS[@]} 逐个检查,别让 pipefail 一刀切。
补上 set -E,让错误有上下文
set -e 只给你一个退出码,不给位置。加上 set -E(errtrace)让 ERR trap 能穿进函数和子 shell,再配一个带 $LINENO 的 trap,出错时至少知道死在哪一行:
#!/usr/bin/env bash
set -Eeuo pipefail
shopt -s inherit_errexit
trap 'echo "[ERR] line $LINENO: $BASH_COMMAND (exit $?)" >&2' ERR
检查清单
- 被
if/||/!调用的函数,内部自己检查返回值; local/export不要和$( )写在同一行;- 算术自增用前置形式或
$(( ))赋值; - 老 bash 上别指望命令替换继承
set -e; - 管道末端有
head/grep -q时,考虑 141 的正常性; trap ... ERR配set -E才有行号。
set -euo pipefail 是好习惯,但它是减少静默失败,不是消除。真正决定脚本可靠性的,仍然是关键步骤的返回值被显式检查过。
- 点赞
- 收藏
- 关注作者
评论(0)