深色模式
脚本健壮性:set -euo pipefail 与日志
摘要:默认 Bash 遇到错误会继续往下跑,这是脚本事故的第一大来源。本文讲清
set -euo pipefail每项的作用与副作用,配合 trap、日志和锁,给出可复用模板。
适用环境
- Linux + Bash 4.x
- 长期运行或由计划任务调用的脚本(备份、巡检、发布)
操作步骤
1. 打开严格模式
bash
set -e # 任何命令返回非 0 就立即退出
set -u # 使用未定义变量视为错误
set -o pipefail # 管道中任意一段失败,整条管道算失败
# 合并写法:set -euo pipefail1
2
3
4
2
3
4
对比:bash -c 'set -e; cd /not-exist; echo hi' 会在 cd 处立即停止,不加 set -e 则继续打印。
2. 处理「预期会失败」的命令
grep 找不到内容返回 1,在 set -e 下会中断脚本,需显式兜底:
bash
if grep -q "nginx" /etc/passwd; then echo "exists"; else echo "missing"; fi
count=$(grep -c "ERROR" app.log || true)1
2
2
3. trap 清理与日志函数
bash
TMPDIR=$(mktemp -d)
trap 'rm -rf "${TMPDIR}"' EXIT # 无论成败都执行
trap 'echo interrupted; exit 130' INT TERM
log() { printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" >&2; }1
2
3
4
2
3
4
日志写 stderr(>&2),这样 ./job.sh > out.txt 不会把业务输出与日志混在一起。
4. 单例锁与完整模板
bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t' # 减少空格分词的意外
exec 9>/var/lock/myjob.lock
flock -n 9 || { echo "already running" >&2; exit 1; }
log() { printf '%s [%s] %s\n' "$(date '+%F %T')" "$1" "$2" >&2; }
trap 'log ERROR "failed at line ${LINENO}"' ERR
log INFO "start"
# ==== 业务逻辑 ====1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
危险
trap ... ERR 捕获不了子 shell $(...) 内的失败。关键步骤仍要显式判断:cmd || { log ERROR failed; exit 1; }。
验证
bash
bash -n job.sh && shellcheck job.sh
bash -c 'set -euo pipefail; false; echo "should not print"'1
2
2
- [ ] 任意命令失败时后续步骤不再执行
- [ ] 使用未定义变量时直接退出,而不是当成空串
- [ ] 重复启动第二个实例被锁挡住
常见坑
set -e 在条件上下文失效
if/&&/|| 中的命令失败不会触发退出,这是 POSIX 规定。
set -u 遇到 $1 未传
用 ${1:-默认值} 或 ${1:?提示} 显式处理,否则直接退出。
IFS 改了没恢复
影响后续所有分词。要么开头一次设定,要么只在子 shell 中临时改。