终端与命令行进阶(8):Shell 脚本自动化实战
上一篇用 Git 门禁脚本演示了只读检查,本篇把它扩展为可投入日常使用的自动化程序:解析参数、校验输入、创建临时目录、可靠清理、原子发布结果,并让日志与业务输出各走正确通道。
一、痛点:能跑一次不等于自动化可靠
交互命令依赖当前目录、已有变量和人工判断,搬进定时任务后这些前提都会消失。脚本要明确解释器、输入、输出、依赖、退出码和副作用。#!/usr/bin/env bash便于寻找 PATH 中的 Bash,固定系统环境可直接写已知绝对路径;若只需 POSIX sh,就不要使用 Bash 数组并误标成 sh。
set -euo pipefail能暴露未定义变量和管道中间失败,却不是完整错误处理。-e在条件、否定和子 Shell 中有细节,预期失败仍应显式if command; then分支。参数始终引用,外部输入作为数据传给命令,避免eval。临时目录用mktemp -d,再用 trap 清理。
二、原理:幂等、原子性与可观察性
幂等意味着相同输入重复运行能得到相同目标状态,不是“第二次什么也不做”。生成文件先写同目录临时文件,校验后mv原子替换,可避免读者看到半成品。跨文件系统的 mv 不保证原子,因此临时文件必须与目标同目录。并发脚本可用flock或创建锁目录,但还需处理进程崩溃后的陈旧锁。
标准输出留给可被管道消费的数据,诊断写标准错误。日志包含 UTC 时间、级别与阶段,不记录令牌。退出码约定 2 为用法错误,其他非零表示运行失败;调用者才能决定重试还是报警。
可靠自动化先把状态变化画成有限步骤:验证、生成临时结果、校验、发布;任一步失败都不得暴露半成品。下面程序在临时目录演示同目录原子替换,并故意保留发布前旧文件可读。程序不需要前文脚本,也不污染当前目录,可作为理解os.replace与幂等输出的最小实验。
frompathlibimportPathfromtempfileimportTemporaryDirectoryimporthashlibimportos records=["alpha.txt\t6","sub/beta.txt\t5"]withTemporaryDirectory()astemp:directory=Path(temp)target=directory/"manifest.tsv"staging=directory/".manifest.tmp"target.write_text("old manifest\n",encoding="utf-8")before=target.read_text(encoding="utf-8").strip()body="path\tsize\n"+"\n".join(records)+"\n"staging.write_text(body,encoding="utf-8")lines=staging.read_text(encoding="utf-8").splitlines()iflines[0]!="path\tsize"orlen(lines)!=3:raiseSystemExit("invalid staging file")digest=hashlib.sha256(staging.read_bytes()).hexdigest()[:12]os.replace(staging,target)after=target.read_text(encoding="utf-8").splitlines()print(f"before={before}")print(f"published_rows={len(after)-1}")print(f"digest={digest}")print(f"staging_exists={staging.exists()}")运行输出:
before=old manifest published_rows=2 digest=0304cab3ecd4 staging_exists=False三、实现:原子生成目录清单
下面程序扫描指定目录,输出带 SHA-256 的 TSV 清单。它不跟随符号链接,NUL 分隔保护空格路径,临时文件位于输出目录,成功校验后才替换正式文件。
#!/usr/bin/env bashset-euopipefailusage(){printf'用法: %s ROOT OUTPUT\n'"$0">&2;}log(){printf'%s level=%s message=%s\n'"$(date-u+%FT%TZ)""$1""$2">&2;}[[$#-eq2]]||{usage;exit2;}root="$1"output="$2"[[-d"$root"]]||{log error'ROOT 不是目录';exit2;}command-vsha256sum>/dev/null2>&1||{log error'缺少 sha256sum';exit127;}root="$(cd"$root"&&pwd-P)"out_dir="$(dirname"$output")"mkdir-p"$out_dir"out_dir="$(cd"$out_dir"&&pwd-P)"output="$out_dir/$(basename"$output")"tmp="$(mktemp"$out_dir/.manifest.XXXXXX")"cleanup(){rm-f--"$tmp";}trapcleanup EXIT HUP INTTERMprintf'sha256\tsize\tpath\n'>"$tmp"whileIFS=read-r-d''file;dohash="$(sha256sum --"$file")"hash="${hash%%*}"size="$(wc-c<"$file")"relative="${file#"$root"/}"[["$relative"!=*$'\n'*&&"$relative"!=*$'\t'*]]||{log warning"跳过无法表示的文件名"continue}printf'%s\t%s\t%s\n'"$hash""$size""$relative">>"$tmp"done<<(find"$root"-typef -print0)awk-F'\t''NR==1 && NF==3 {ok=1} END {exit !ok}'"$tmp"chmod0644"$tmp"mv-f--"$tmp""$output"trap- EXIT HUP INTTERMlog info"manifest=$output"用临时夹具做黑盒测试,不依赖别篇产物。测试既验证成功内容,也验证错误输入的非零退出;真实仓库应把被测脚本路径作为参数传入。
set-euopipefailfixture="$(mktemp-d)"trap'rm -rf -- "$fixture"'EXITmkdir-p"$fixture/input/sub"printf'alpha\n'>"$fixture/input/a file.txt"printf'beta\n'>"$fixture/input/sub/b.txt"script="${1:?请传入清单脚本路径}"bash"$script""$fixture/input""$fixture/manifest.tsv"test-s"$fixture/manifest.tsv"test"$(wc-l<"$fixture/manifest.tsv")"-eq3grep-F$'size\tpath'"$fixture/manifest.tsv">/dev/nullifbash"$script""$fixture/missing""$fixture/bad.tsv";thenecho'错误输入不应成功'>&2exit1fiprintf'all tests passed\n'四、踩坑:清理钩子也会制造事故
trap 中删除路径前,变量必须已初始化且指向明确临时目标,绝不能对空变量做递归删除。本文只删除mktemp返回的文件。通配符在没有匹配时的行为因 Shell 选项而异,批处理优先 find 的 NUL 输出。读取文本时用IFS= read -r,否则反斜杠和首尾空白可能改变。
Shell 适合编排命令,不适合复杂数据结构、浮点计算和大型并发状态机。超过几百行、需要跨平台库或严谨 JSON 处理时,应迁移到 Python、Go 等语言,同时保留相同 CLI 契约。优化前先用真实输入测量,减少外部进程调用通常比微调语法更有效。
五、验证:从失败路径开始测试
除正常夹具外,还要测试只读输出目录、磁盘空间不足、依赖缺失、中断信号、并发运行和文件在扫描期间变化。运行 ShellCheck 与bash -n,再在干净容器或 CI 中确认未依赖个人 PATH。若脚本会写生产数据,增加 dry-run、锁和备份,并在小范围演练恢复。
下一篇把自动化带到远端:用 SSH config、主机密钥、连接复用和跳板机建立既快又不牺牲身份校验的远程工作流。
参考来源
- GNU Bash Manual:Bourne Shell Builtins
- ShellCheck Wiki
- GNU Coreutils:mktemp
👍 觉得有用就点个赞 + 收藏,方便回头查阅;有疑问直接在评论区留言,我看到都会回。
🚀 本文属于《终端与命令行进阶》系列,持续更新,关注不迷路。
📌 文章里的代码都能直接跑。想要可直接 clone 的完整工程 + 配套部署脚本 / 踩坑清单?评论一声或发邮件到cj2664@qq.com,我免费发你。
如果你正好在做类似系统、或有工程化难题想找人做,也欢迎邮件聊一句——我按实际情况评估,能落地的就接单或出方案。评论和邮件都能直接找到我,不用跳别的平台。
