← 返回文章列表

Shell 脚本要点:安全开头、引用规则与四个常见错误

命令行避坑

先加这四行安全开头

#!/usr/bin/env bash
set -e          # 任一命令失败即退出
set -u          # 使用未定义变量时报错
set -o pipefail # 管道中任一环节失败即视为失败
IFS=$'\n\t'    # 让分词只按换行与制表符,避免空格踩坑

没有这几行,脚本常常「看起来执行完了」,实则中间某步失败被静默跳过。

引用规则:绝大多数变量要加双引号

写法行为建议
$var会再分词与通配展开几乎不要用
"$var"原样作为一个参数默认写法
${var}便于拼接边界,如 "${var}_suffix"拼接时用
'$(cmd)'单引号内不执行需要字面量时用

四个最容易出事的地方

  1. 路径含空格:未加引号时,一个文件名会被拆成多个参数;
  2. cd 失败后继续执行:cd /some/dir && do_something,不要单独一行 cd;
  3. rm 与变量:rm -rf "$DIR"/* 在 DIR 为空时可能变成危险的删除,应先判空并用 set -u 兜底;
  4. 管道吞掉错误: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 运行。脚本需要单元测试吗?超过几十行、被多人使用的脚本值得加;至少做一次语法检查与空参数测试。命令找不到怎么办?显式判断依赖是否存在,而不是让错误发生在中途。

什么时候该换语言

当脚本开始处理复杂数据结构、需要单元测试或跨平台分发时,继续用脚本语言维护的成本会超过收益。此时应果断迁移到具备类型与包管理能力的语言,而不是把脚本不断加长。

脚本的测试方式

脚本也应有最基本的测试:用固定输入运行并断言输出与退出码。可以借助通用的测试框架,也可以用简单的断言函数自建。哪怕只覆盖主要分支,也能显著减少“改一处坏一处”的情况。