set -euo pipefail脚本开头第一组命令:遇错停、用未定义变量停、管道失败也停。
约 1420 字大约 5 分钟
2026-05-14
Shell 脚本默认的行为出奇地“宽容”:一条命令失败了,它若无其事地继续往下跑;用了一个根本没定义的变量,它当成空字符串处理。这种宽容在自动化场景里是灾难。set -euo pipefail 这一行,就是把这些“宽容”改成“严格”。
理解 Shell 默认的危险宽容行为,用 set -e/-u/-o pipefail 让脚本遇错即停,配合 shellcheck 和 trap 写出可靠的自动化脚本。
set -euo pipefail脚本开头第一组命令:遇错停、用未定义变量停、管道失败也停。
bash -n script.sh不执行只查语法,写完先过一遍。
shellcheck script.sh自动揪出引号、变量、逻辑等常见隐患,强烈建议常用。
${VAR:?未设置}VAR 没设置就直接报错退出,比默默用空值安全。
看这段脚本,它的默认行为很危险:
#!/usr/bin/env bash
cd "$build_dir" # 假设这里 cd 失败了(目录不存在)
rm -rf ./* # 但脚本不会停!现在在原来的目录里执行 rm -rfcd 失败了,Shell 默认不会停,继续执行 rm -rf ./*——而此时当前目录还是脚本启动时的目录。这就是 Shell 默认行为能酿成大祸的典型例子。
把这一行加在脚本开头(紧跟 shebang 之后),它是三个开关的组合:
| 开关 | 作用 | 解决什么 |
|---|---|---|
set -e | 任何命令失败(退出码非 0),脚本立即退出 | “一步错了还继续往下跑” |
set -u | 引用未定义的变量时报错退出 | “变量名打错了,被当成空值” |
set -o pipefail | 管道里任何一段失败,整条管道就算失败 | “`a |
#!/usr/bin/env bash
set -euo pipefail加上它,前面那个 cd 失败的例子里,脚本会在 cd 那一步就停下,根本走不到 rm。
默认情况下,一条管道的退出码只看最后一个命令:
grep "ERROR" missing.log | wc -l
# grep 失败了(文件不存在),但 wc 成功了
# 整条管道退出码是 0 —— 被判成功!set -o pipefail 改掉这个:只要管道中任何一段失败,整条就算失败。在依赖管道结果做判断的脚本里,这一条能避免“基于错误数据继续执行”。
set -e 很有用,但有些情况要知道:有些命令“失败”是预期内的(比如 grep 没匹配到返回 1),这时不希望脚本退出,可以显式处理:
if grep -q "pattern" file; then ...; fi # 放进 if,set -e 不会因它退出
count=$(grep -c "x" file || true) # || true 兜底,明确表示“失败也没关系”关键是显式——明确写出“这里允许失败”,而不是依赖默认的宽容。
set -euo pipefail 是运行时的保护,shellcheck 是写代码时的保护。它是一个静态分析工具,能自动揪出:变量没加引号、用了未定义变量、[ ] 用法错误、无意义的 cd……
shellcheck script.sh把它当成 Shell 脚本的“编译器警告”——养成每个脚本都过一遍 shellcheck 的习惯,能消灭掉大半的低级 bug。配合 脚本日志与错误处理 里的 trap,运行时和编写时两道防线就齐了。
set -euo pipefail 是底线,不是借口
加上 set -euo pipefail 让脚本“遇错即停”,大幅降低了脚本带病运行的风险——但它不能替代测试。它能保证“出错就停”,不能保证“逻辑正确”。一个把 $src 和 $dst 写反的备份脚本,set -e 拦不住。所以完整的安全习惯是:① 开头 set -euo pipefail;② 写完过 shellcheck;③ 危险命令先 echo 预览;④ 测试环境验证;⑤ 关键变量用 ${VAR:?} 强制校验。这一行是底线,不是全部。
| 误区 | 更稳妥的做法 |
|---|---|
脚本不加 set -euo pipefail,靠默认行为 | 开头就加上,把“宽容”改成“遇错即停” |
以为加了 set -e 脚本就一定安全 | 它只保证出错停,不保证逻辑对,仍要测试和 review |
预期会失败的命令也被 set -e 中断 | 放进 if,或用 ` |
| 从不用 shellcheck | 每个脚本都过一遍,它能抓出大半低级 bug |
版权归属:Shuo Liu