写更稳健的 Bash 脚本
Shell 脚本写起来快,出问题也快:某条命令失败了脚本却继续往下执行,文件名里有个空格就删错了文件。下面几条习惯不复杂,但能挡掉大部分意外。
1. 开启严格模式
在脚本开头紧跟 shebang 写上:
#!/usr/bin/env bash
set -euo pipefail
-e:命令返回非零状态时立即退出。-u:引用未定义的变量时报错,避免拼写错误变成空字符串。-o pipefail:管道中任意一段失败,整个管道就算失败;否则只看最后一个命令。
set -e 并非万能:出现在 if、while 条件里,或者
&&、|| 左侧的命令失败时不会触发退出。确实允许失败的命令,可以显式写成 command || true,让意图更清楚。
2. 变量一律加引号
不加引号的变量会被按空白拆分并展开通配符,这是 Shell 里最常见的坑:
dir="My Documents"
rm -rf $dir/cache # 实际变成 rm -rf My Documents/cache
rm -rf "$dir/cache" # 正确
数组同理,用 "${files[@]}" 展开,每个元素都会作为独立参数传递。
另一个相关的保险是 ${var:?}:变量为空或未设置时直接报错退出。对 rm -rf "${target:?}/"* 这样的危险操作尤其有用。
3. 检查参数和依赖
脚本开始干活之前,先确认输入是对的、需要的命令都在:
usage() {
echo "用法: $(basename "$0") <源目录> <目标目录>" >&2
exit 2
}
[[ $# -eq 2 ]] || usage
[[ -d "$1" ]] || { echo "源目录不存在: $1" >&2; exit 1; }
for cmd in rsync tar; do
command -v "$cmd" >/dev/null 2>&1 || { echo "缺少命令: $cmd" >&2; exit 1; }
done
错误信息写到标准错误(>&2),这样即使标准输出被重定向,用户也能看到提示。
4. 用 trap 清理临时文件
临时文件用 mktemp 创建,不要自己拼 /tmp/xxx,以免文件名冲突。再配合 trap,脚本无论正常结束还是中途出错,都会执行清理:
workdir="$(mktemp -d)"
cleanup() { rm -rf "$workdir"; }
trap cleanup EXIT
5. 一个可复用的模板
把上面几条合在一起,就是我新建脚本时的起点:
#!/usr/bin/env bash
# backup.sh — 把目录打包备份到指定位置
set -euo pipefail
usage() {
echo "用法: $(basename "$0") <源目录> <备份目录>" >&2
exit 2
}
log() { printf '[%s] %s\n' "$(date '+%F %T')" "$*" >&2; }
main() {
[[ $# -eq 2 ]] || usage
local src="$1" dest="$2"
[[ -d "$src" ]] || { log "源目录不存在: $src"; exit 1; }
mkdir -p "$dest"
# 不用 local:EXIT trap 在 main 返回后才执行,需要访问全局变量
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT
local name
name="$(basename "$src")-$(date +%Y%m%d-%H%M%S).tar.gz"
tar -czf "$workdir/$name" -C "$(dirname "$src")" "$(basename "$src")"
mv "$workdir/$name" "$dest/"
log "已生成 $dest/$name"
}
main "$@"
把逻辑放进 main 函数、在最后一行调用,有两个好处:变量可以用 local 限定作用域;脚本在下载过程中被截断时,因为最后一行还没出现,不会执行半截内容。
6. 交给 ShellCheck
ShellCheck 是一个静态检查工具,能发现漏写引号、错误的条件判断等大量问题。多数发行版可以直接从软件源安装:
sudo apt install shellcheck # Debian / Ubuntu
shellcheck backup.sh
每条警告都带编号(如 SC2086),搜索编号就能找到详细解释和修复建议。
小结
严格模式让错误尽早暴露,引号避免意外展开,参数检查和 trap 让脚本行为可预期,ShellCheck 负责查漏补缺。脚本一旦超过一两百行、或者开始需要复杂的数据结构,就该考虑换成 Python 之类的语言了。