90% 的脚本都缺这三行
-e 命令失败就退出,-u 未定义变量报错,-o pipefail 管道任一失败算失败。缺这三行,你的脚本就是一颗定时炸弹。
随便打开一个团队的 Shell 脚本,第一行通常是 #!/bin/bash,然后直接开干。问题是 Bash 默认行为是宽容到危险:
a | b 里 a 崩了,b 用空输入跑,结果全错
最经典的惨案:rm -rf $FOO/bar,本意删某个目录下的 bar,但 $FOO 拼错了或没定义,Bash 把它当空字符串,命令变成 rm -rf /bar——从根目录开始删。加了 set -u,$FOO 未定义直接报错退出,惨案不会发生。set -u 是 rm 的最后一道防线。
下面是几段真实风格的脚本,点"体检"看它缺哪些防护、得多少分。三个维度:-e(失败退出)、-u(变量检查)、pipefail(管道安全)。
set -e:命令返回非零就退出。但有个坑——if/while/&&/|| 里的命令失败不触发 -e(因为它们本来就是要"测试"失败)。所以 if grep ...; then 里 grep 没找到不会让脚本退出,这是设计如此,不是 bug。
set -u:引用未定义变量就报错退出。但 ${VAR:-default} 和 ${VAR:+x} 这种带默认值的写法不触发 -u,所以你想要"未定义就用默认值"的变量,用 ${VAR:-} 包一下即可。
set -o pipefail:管道的退出码取所有命令里最大的非零,而不是默认的"只看最后一个"。没有它,false | true 退出码是 0(true 的),脚本以为成功了;有了它,退出码是 1(false 的),-e 能捕获到。pipefail 是 -e 在管道里生效的前提。
加了三件套还不够,还有几个常见坑:
cd /some/dir; rm -rf * 如果目录不存在 cd 失败,但脚本还在原目录跑 rm -rf *——删错地方。用 cd /some/dir || exit 1 或 set -e 兜底。(some_cmd) 子 shell 默认继承 set,但 bash script.sh 调用的子脚本要自己加 set,不会自动继承。trap 'rm -f $TMP' EXIT 在退出时清理,比手动删可靠。IFS 解析后要还原,否则后续命令行为变了。或用子 shell (IFS=...; ...) 隔离。一个真正健壮的脚本开头长这样:#!/usr/bin/env bash; set -euo pipefail; IFS=$'\n\t'。最后一行把字段分隔符设成只认换行和 tab,避免空格把文件名切碎——和 find 的 -print0 同一个道理。健壮不是加一行 set,是一套防护组合。