目标
脚本跑到一半挂掉不可避免,重跑时重复副作用却完全可以避免。严格模式、创建前的存在性守卫、完成标记与“收敛而非追加”的变更方式,把脆弱的引导脚本变成事故现场也敢直接重跑的东西。验证手段很硬核:连跑两遍并 diff 文件世界,第二次必须是空操作。
步骤要点
- 脚本第一件事开启 set -euo pipefail 加收紧的 IFS,让第一个真实错误就停止而不是带病狂奔。
- 所有创建动作加守卫:grep -qxF 判断行存在再追加、mkdir -p、模板文件只在不存在时复制、临时文件加原子 mv。
- 用 step 函数包住每个步骤并落 .done 标记,重跑时已完成步骤打印 skip 并跳过,崩溃后自动续跑。
- 状态变更改为收敛式:配置整文件从 heredoc 重生成而不是追加行,用户与用户组用存在性判断加 usermod 幂等追加。
- 幂等性验证:跑两遍并对比两次之后变更过的文件清单,diff 为空且第二遍日志全是 skip 才算达标。
排错提示
某步骤只做到半幂等就拆开:破坏性部分用标记条件化,收敛部分每次都跑,数据库迁移同理用版本表;代码改了标记还让步骤跳过,就把步骤定义的哈希编进标记名,改定义自动失效旧标记;严格模式让旧脚本因未加引号的变量报错,要修脚本而不是关模式,用默认值语法补齐并把这当作补上的评审。
适用边界
幂等是按副作用计的:发邮件、扣款这类外部副作用需要脚本结构之外的幂等键;标记文件假设主机稳定,容器化一次性运行应从声明输入推导状态。
代码示例
# The rerun test: second run must be a no-op
bash setup.sh > run1.log
find /etc/app* "$HOME" -newer marker -type f | sort > s1.txt
touch marker
bash setup.sh > run2.log
find /etc/app* "$HOME" -newer marker -type f | sort > s2.txt
diff s1.txt s2.txt # empty = nothing changed
run2.log:
# skip: create_user (already done)
# skip: install_deps (already done)
# render_conf: regenerated (converges, no growth)