Shell 脚本健壮性实战:set -euo pipefail 救不了你的五种情况
Shell 脚本健壮性实战:set -euo pipefail 救不了你的五种情况
写运维脚本时,set -euo pipefail 几乎成了标配。但把它当成"安全网"往往会在最关键的场景失效——脚本该报错时静默通过,该中断时继续往下跑。这篇文章拆五个真实踩过的坑,每个都给出可照抄的写法。
一、-e 在管道里只认最后一个命令
set -e 判断的是整条管道最后一个命令的退出码。前面命令失败了,只要最后那个 wc -l 成功,整行依然算成功。
# 危险:tar 解包失败,wc 依然成功,脚本继续
tar -xzf app.tar.gz | wc -l
pipefail 就是为这个而生的:只要管道中任一命令失败,整条管道返回非零。但注意它只覆盖管道,覆盖不了下面这些情况。
二、命令替换里的失败被吞掉
set -euo pipefail
count=$(grep -c ERROR app.log) # grep 没匹配到会返回 1
echo "错误数:$count"
grep -c 无匹配时退出码是 1,命令替换的失败却不会触发 -e(赋值语句本身总是成功)。脚本继续往下跑,count 还是空的。
两种修法,按语义选:
count=$(grep -c ERROR app.log || true) # 允许为空,空视为 0
count=$(grep -c ERROR app.log) || exit 1 # 不允许为空,直接失败
三、[[ ]] 与 (( )) 里的 -e 陷阱
(( i++ )) 在 i 为 0 时返回 1(因为表达式的值是 0),在 set -e 下会直接退出脚本。这是循环计数器最经典的翻车点。
set -e
i=0
(( i++ )) # 这里脚本就退出了,因为返回 1
echo "永远打印不到"
改法:(( i++ )) || true,或者干脆写 i=$((i+1))。
四、local 与函数返回值会掩盖错误
set -e
f() { local x=$(false); echo "不会到这里"; }
f
local x=$(false) 的退出码是 local 自身的(总是 0),-e 看不到 false 的失败。同理,函数体最后一句如果不是失败命令,函数的退出码就是那一句的。
稳妥写法:先声明再赋值,让赋值语句自己暴露失败。
f() {
local x
x=$(false) || return 1
echo "不会到这里"
}
五、后台任务与 wait 不参与 -e
cmd & 启动的后台进程失败,脚本完全不知道;wait 不带参数返回的是最后一个被等待进程的状态。
set -euo pipefail
job1 & p1=$!
job2 & p2=$!
wait $p1; wait $p2 # 必须逐个 wait 并检查
一份可以直接抄的脚本骨架
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'
trap 'echo "FAILED at line $LINENO: $BASH_COMMAND" >&2' ERR
trap 'rm -rf "$TMPDIR_WORK"' EXIT
TMPDIR_WORK=$(mktemp -d)
-E让 ERR trap 对函数、子 shell 也生效,配合$LINENO能直接定位翻车行号;IFS=$'\n\t'避免文件名带空格时被拆词;trap ... EXIT清理临时目录,比在每个分支末尾手写rm可靠得多。
三条自检清单
- 每个
$( )都问一句"它失败了会怎样",要么|| true要么|| exit。 - 所有
(( ))递增/判断外面套|| true,或者改用$(( ))赋值形式。 - 每个
&都配一个wait $pid,脚本末尾用wait兜底前先看是否还有遗留子进程。
set -euo pipefail 是必要的起点,不是终点。真正让脚本可靠的是"每个可能失败的地方都显式表态"——这比任何一行 set 都管用。
- 点赞
- 收藏
- 关注作者
评论(0)