先加这四行安全开头
#!/usr/bin/env bash
set -e # 任一命令失败即退出
set -u # 使用未定义变量时报错
set -o pipefail # 管道中任一环节失败即视为失败
IFS=$'\n\t' # 让分词只按换行与制表符,避免空格踩坑
没有这几行,脚本常常「看起来执行完了」,实则中间某步失败被静默跳过。
引用规则:绝大多数变量要加双引号
| 写法 | 行为 | 建议 |
|---|---|---|
| $var | 会再分词与通配展开 | 几乎不要用 |
| "$var" | 原样作为一个参数 | 默认写法 |
| ${var} | 便于拼接边界,如 "${var}_suffix" | 拼接时用 |
| '$(cmd)' | 单引号内不执行 | 需要字面量时用 |
四个最容易出事的地方
- 路径含空格:未加引号时,一个文件名会被拆成多个参数;
- cd 失败后继续执行:
cd /some/dir && do_something,不要单独一行cd; - rm 与变量:
rm -rf "$DIR"/*在 DIR 为空时可能变成危险的删除,应先判空并用 set -u 兜底; - 管道吞掉错误:
cmd | head即使 cmd 失败,退出码也可能为 0,这正是 pipefail 存在的意义。
可维护脚本的几个习惯
- 参数校验放最前:缺少参数就打印用法并退出;
- 用 trap 做清理:临时目录、锁文件在退出时统一释放;
- 日志带时间且可关:便于排查,也避免污染调用方输出;
- 长脚本加 --dry-run:先打印将要执行的命令,确认无误再真跑。
set -euo pipefail
usage() { echo "usage: $0 <target-dir>"; exit 1; }
[ $# -eq 1 ] || usage
target=$1
[ -d "$target" ] || { echo "not a directory: $target" >&2; exit 1; }
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT # 无论成功失败都会清理
常见问题
sh 和 bash 有区别吗?有。数组、[[ ]]、pipefail 都是 bash 特性,脚本里要用就明确 #!/usr/bin/env bash,并避免用 sh script.sh 运行。脚本需要单元测试吗?超过几十行、被多人使用的脚本值得加;至少做一次语法检查与空参数测试。命令找不到怎么办?显式判断依赖是否存在,而不是让错误发生在中途。
什么时候该换语言
当脚本开始处理复杂数据结构、需要单元测试或跨平台分发时,继续用脚本语言维护的成本会超过收益。此时应果断迁移到具备类型与包管理能力的语言,而不是把脚本不断加长。
脚本的测试方式
脚本也应有最基本的测试:用固定输入运行并断言输出与退出码。可以借助通用的测试框架,也可以用简单的断言函数自建。哪怕只覆盖主要分支,也能显著减少“改一处坏一处”的情况。