写更稳健的 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 之类的语言了。