Shell 脚本通用要点
适用于所有用 shell 完成的工作:部署和运维脚本、构建脚本、一次性的命令行操作。示例以 bash 为准。
一、动手前
- 先确认目标和流程。 写下脚本要从什么状态到什么状态、中间有哪些步骤、哪一步之后不能回头。流程有多种做法时,先和使用者确认,从最简单的那种开始。
- 先看一眼要改动的对象。 删除、覆盖、移动之前,确认目标路径、分支、主机和自己设想的一致。
- 能用现成工具就不自己造。 优先用系统自带的命令,只有确实需要时才调用应用代码或额外的运行时。
二、失败要安全
- 出错就停。 开头写
set -Eeuo pipefail,再加trap 'printf "第 %s 行失败(退出码 %s)\n" "$LINENO" "$?" >&2' ERR,失败时能看到是哪一行。 - 操作要可逆,影响范围要小。 改名保留,少用删除。改动前先备份,备份放在脚本固定管理的位置。
rm -rf --只用于脚本自己创建、路径固定的目录,路径绝不能来自可能为空的变量。 - 半途失败要回到能用的状态。 在改动线上状态之前注册
trap ... EXIT,用来撤回改动或恢复旧服务。到达“不能自动撤回”的那一步时,用trap - EXIT取消。 - 不能自动撤回的失败,给出下一步命令。 例如“启动失败,执行
bash xxx rollback”。会丢数据的恢复由人决定,不自动执行。 - 可以重复执行。 第二次运行不会破坏第一次的结果:已经完成的步骤要么跳过,要么明确拒绝并说明原因。
三、检查要准
- 检查的必须是真正关心的东西。 “接口返回 200”比“进程存在”更能说明服务可用,“读得到数据库的接口返回 200”又比“首页返回 200”更可靠。
- 在不会变化的数据上做严格比对。 比对前后状态时,先让数据停止变化(停服务、取快照),再要求完全相等。不要在一直变化的数据上设容忍规则。
- 不写死只对一次运行成立的值。 具体的版本号、行数、条目数、文件内容标记,下一次运行就会误报。
- 有差异就列出明细。 打印“前 → 后”,再由人确认。不确定时默认取消。
- 能自动查到的事实,不让人回答。 只有判断本身需要人来做时才询问。
四、交互
- 确认词按后果分级。 只是提醒、可以继续的用
y;停机、删除、改数据用yes。不提供跳过所有确认的开关。 - 非交互时默认取消。
read -r -p '…' answer || true,空答案当作取消。 - 输出分层。 步骤、警告、错误各用固定的前缀;警告和错误写到 stderr;错误以非零状态退出。每一步结束时告诉使用者下一步做什么。
- 不打印凭据。 密码、令牌、私钥、Cookie 不出现在输出、日志和命令行参数里,需要时引用文件路径。
五、写法
- 常量放在顶部。 路径、服务名、端口写成大写常量。函数里的变量用
local。 - 只调用一次的逻辑直接内联。 被多处调用的才写成函数。不要为了“看起来整齐”拆出只用一次的小函数。
- 子命令少而完整。 每个子命令完成一件完整的事。不带参数时打印用法,并以 2 退出。
- 依赖在入口处检查。 用
command -v检查要用的命令,缺少时直接告诉使用者怎么安装,不要等到中途才失败。 - 注释只写原因。 代码已经说明了在做什么,注释解释为什么这样做。
- 变量一律加引号。
"$var"、"${arr[@]}"。路径参数前加--,防止以-开头的值被当成选项。
六、常见陷阱
&&、||链里的失败不会触发set -e。 必须失败即停的链写成a && b || die '原因'。local x=$(cmd)会吞掉退出码。 先写local x,再写x=$(cmd)。- 命令替换直接作为参数时,失败不会让脚本停下。 例如
tool "$(生成参数)"。先赋给变量,再使用。 pipefail下,cmd | grep -q可能因为 SIGPIPE 得到错误的退出码。 先out=$(cmd),再用case或grep处理这个变量。- 函数或循环的最后一行是
[ 条件 ] && 动作时,条件为假会让返回值非零。 改用if。 - 通配符没有匹配时会原样保留。 使用前先用
[ -e ]判断,或者开启nullglob。 - 相对路径依赖当前目录。 给函数和外部命令传完整路径。很多工具遇到不存在的路径会默默新建,例如 sqlite3 会新建一个空库,检查反而“通过”。
cd会影响后面的所有命令。 需要时放进子 shell:(cd dir && cmd)。sudo cmd > file的重定向不在 sudo 里执行。 改用cmd | sudo tee file。printf '%-10s'按字节计算宽度。 中文等多字节字符会对不齐,改用“名称:值”的格式。- heredoc 是否展开变量取决于结束标记的引号。
<<'EOF'不展开,<<EOF展开。嵌入其他语言的代码时用带引号的形式。 - Windows 编辑的文件可能带 CRLF。 脚本和要比较的命令输出都要确认没有
\r。
七、验证
bash -n只能检查语法。 能装 shellcheck 就跑一遍,但它也代替不了实际运行。- 没有真实环境时搭沙箱。 把路径常量改到临时目录,在
PATH前面放桩命令代替systemctl、curl、sudo等,核心逻辑用真实依赖执行。 - 逐个场景运行。 正常流程、使用者取消、每一种检查失败、恢复、重复执行。每个场景结束后检查服务状态、目录和文件内容,不只看退出码。
- 测试本身也要可信。 一项检查从来没失败过时,先确认它真的检查到了东西。
- 写清楚没验证的部分。 哪些靠桩代替、哪些只能在真实环境确认,都要明确写出来,不要把沙箱结果当作真实环境的结果。