Shell 脚本健壮性实战:set -euo pipefail 与 trap 让脚本 fail fast
很多 Shell 脚本在开发机上跑得好好的,一上生产就悄悄出问题:中间某条命令失败了,脚本却继续往下跑,最后把错误结果当成成功,还可能删错文件、发错通知。根源往往不是逻辑写错,而是脚本默认太“宽容”:命令失败不退出、变量拼错当空串、管道中间失败被掩盖。本文结合一次真实的线上事故,讲清楚 set -euo pipefail 与 trap 的组合用法,让脚本 fail fast:出错立刻停下来,而不是带着错误继续跑。
一、默认模式为什么危险
默认的 Bash 有三个“坑”。第一,命令失败不退出:cp 失败了,脚本照样执行后面的 rm,源文件没拷走,旧文件却被删了。第二,未定义变量当空串用:把 $BACKUP_DIR 拼错成 $BACKUP_DRI,路径变成空,rm -rf 指向了意想不到的位置。第三,管道只看最后一个命令的退出码:grep 没找到内容返回 1,但管道接了 wc -l,整体退出码是 0,脚本以为一切正常。这三个坑单独看都不大,合在一起就是一次线上事故。
二、一行配置堵住三个坑
在脚本开头写上这一行,是性价比最高的健壮性改造:
#!/bin/bash
set -euo pipefail
其中 -e 表示任一命令失败立即退出;-u 表示引用未定义变量立即报错退出;-o pipefail 表示管道中任一命令失败,整个管道就算失败。注意顺序:这行要放在所有业务逻辑之前,放在变量赋值和函数定义之前才最有效。改造后,上面三个坑都会在第一时间暴露,而不是在凌晨三点由用户先发现。
三、trap:出错时留下现场再退出
光退出还不够,线上排障最怕“死得不明不白”。用 trap 捕获 ERR 和 EXIT 信号,出错时自动打印行号、失败命令和关键变量,再做清理:
trap 'echo "[ERROR] 第 $LINENO 行失败:$BASH_COMMAND" >&2; rm -f "$TMP_FILE"' ERR
trap 'rm -f "$TMP_FILE"' EXIT
这样脚本退出前会先删掉临时文件,不留垃圾;错误日志里有行号和命令,复现问题不用再靠猜。需要注意,trap ERR 对条件判断里的失败命令不触发,这是符合预期的:if grep -q xxx; then 这种写法本身就是在处理失败,不应算作异常。
四、需要“容错”的地方要显式写出来
set -e 不是让所有失败都致命,而是让“意外的失败”致命。确实允许失败的命令,要显式表达意图,例如:
grep -q "ready" log.txt || true
count=$(grep -c "WARN" app.log || true)
这样写的好处是意图清晰:后来维护的人一眼就能看出,这里的失败是预料之中的,而不是被 set -e 漏掉的。反之,如果到处写 || true 来“灭火”,就等于把 fail fast 又关掉了,宁可逐个审视。
五、一个可直接套用的脚本骨架
把上面的做法固化成骨架,新脚本直接复制使用:
#!/bin/bash
set -euo pipefail
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP_FILE="$(mktemp)"
trap 'echo "[ERROR] 第 $LINENO 行失败:$BASH_COMMAND" >&2' ERR
trap 'rm -f "$TMP_FILE"' EXIT
main() {
echo "开始执行:$(date)"
# 业务逻辑写在这里
}
main "$@"
配合 shellcheck 做静态检查、关键步骤加 echo 日志、危险操作先 dry-run,这套组合基本能覆盖日常运维脚本的大部分翻车场景。
结语
Shell 脚本的健壮性不靠小心翼翼地逐行检查,靠的是让错误无处藏身:set -euo pipefail 让失败立刻现形,trap 让失败留下现场,显式的 || true 让容错意图明确。下次写脚本时,先在文件头写上这两行,再写业务逻辑,你会发现排障时间少了一大半。
- 点赞
- 收藏
- 关注作者
评论(0)