裸脚本的三种「静默失败」
Bash 默认的容错行为是排障地狱的根源:命令失败不会退出(-e 未开)、未定义变量当空字符串用(-u 未开)、管道只看最后一个命令的退出码(pipefail 未开)。
后果是脚本「看起来跑完了」,实际中途就错了:cd 失败还在原目录继续删文件、变量名打错静默变空、grep | head 掩盖上游崩溃。线上事故往往就藏在这几行里。
cd /data/release && rm -rf old/ # cd 失败时 rm 根本不该执行
cp app.conf /etc/app.conf # 失败了脚本照样往下走
echo "deploy done" # 假象:一切都好第一道防线:set -euo pipefail
shebang 之后立刻加上 set -euo pipefail:-e 任一命令非零即退出;-u 引用未定义变量直接报错而非当空串;-o pipefail 让管道以第一个失败命令的退出码为准。
注意 -e 的例外:条件上下文(if/while/&&/||)中的失败不会触发退出——这是特性不是 bug,cmd || exit 1、if grep -q ... 这类写法依然要显式处理。
#!/usr/bin/env bash
set -euo pipefail
# 调试期可加:逐条回显执行的命令
# set -x清理现场:trap 保底
脚本中途失败或被 Ctrl+C 杀掉时,临时文件、后台子进程、锁文件都会残留。用 trap 挂在 EXIT 上统一清理:无论正常结束、报错退出还是被中断,清理函数都会执行。
临时目录用 mktemp -d 创建(不要拼固定路径,避免并发冲突与符号链接攻击),清理函数只删自己创建的目录。
tmpdir=$(mktemp -d)
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT
# 需要 Ctrl+C / kill 也走一遍显式清理时可再加:
# trap cleanup INT TERM引号纪律:所有展开都加双引号
未加双引号的变量会在空格处拆词、空变量直接消失:rm -rf $dir 在 dir 为空时等于裸 rm -rf(灾难级)。纪律很简单——所有 $var 一律写成 "$var"。
三个高频配套写法:文件名可能以 - 开头时用 -- 分隔符或 ./ 前缀;必填参数用 ${1:?提示} 缺失即报错退出;可选值用 ${2:-默认值}。
rm -rf -- "$dir" # -- 防止文件名被当成选项
src="${1:?用法: $0 <源目录>}" # 缺参数直接报错退出
out="${2:-./dist}" # 提供默认值输入校验与幂等
外部输入(位置参数、环境变量、find 的结果)使用前先验证:依赖的命令用 command -v 检查,路径用 [ -d ]/[ -f ] 检查。宁可启动时报错,不要跑一半才崩。
让脚本可重复执行(幂等):mkdir -p 代替 mkdir,cp -f/rsync 代替裸 cp。半途失败的脚本可以直接重跑,不用人工清场。
command -v jq >/dev/null || { echo "需要 jq" >&2; exit 1; }
[ -d "$src" ] || { echo "目录不存在: $src" >&2; exit 1; }
mkdir -p "$out" # 幂等:已存在不报错上生产前的调试与清单
三个工具覆盖九成问题:bash -n 只查语法,bash -x 逐行回显执行过程,shellcheck 静态分析能抓住上面几乎所有坑(包括未加引号的变量)。把 shellcheck 接进 CI,脚本质量问题基本绝迹。
最终清单:shebang + set -euo pipefail + trap 清理 + 全变量双引号 + 入参校验 + 幂等写法 + shellcheck 通过。照抄这套骨架,脚本就从「碰运气」变成「可信」。
bash -n deploy.sh # 只查语法
bash -x deploy.sh # 逐行回显执行(调试)
shellcheck deploy.sh # 静态分析(推荐装进 CI)