终端与命令行进阶(8):Shell 脚本自动化实战

发布时间:2026/8/17 2:29:35
终端与命令行进阶(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:directoryPath(temp)targetdirectory/manifest.tsvstagingdirectory/.manifest.tmptarget.write_text(old manifest\n,encodingutf-8)beforetarget.read_text(encodingutf-8).strip()bodypath\tsize\n\n.join(records)\nstaging.write_text(body,encodingutf-8)linesstaging.read_text(encodingutf-8).splitlines()iflines[0]!path\tsizeorlen(lines)!3:raiseSystemExit(invalid staging file)digesthashlib.sha256(staging.read_bytes()).hexdigest()[:12]os.replace(staging,target)aftertarget.read_text(encodingutf-8).splitlines()print(fbefore{before})print(fpublished_rows{len(after)-1})print(fdigest{digest})print(fstaging_exists{staging.exists()})运行输出beforeold manifest published_rows2 digest0304cab3ecd4 staging_existsFalse三、实现原子生成目录清单下面程序扫描指定目录输出带 SHA-256 的 TSV 清单。它不跟随符号链接NUL 分隔保护空格路径临时文件位于输出目录成功校验后才替换正式文件。#!/usr/bin/env bashset-euopipefailusage(){printf用法: %s ROOT OUTPUT\n$02;}log(){printf%s level%s message%s\n$(date-u%FT%TZ)$1$22;}[[$#-eq2]]||{usage;exit2;}root$1output$2[[-d$root]]||{log errorROOT 不是目录;exit2;}command-vsha256sum/dev/null21||{log error缺少 sha256sum;exit127;}root$(cd$rootpwd-P)out_dir$(dirname$output)mkdir-p$out_dirout_dir$(cd$out_dirpwd-P)output$out_dir/$(basename$output)tmp$(mktemp$out_dir/.manifest.XXXXXX)cleanup(){rm-f--$tmp;}trapcleanup EXIT HUP INTTERMprintfsha256\tsize\tpath\n$tmpwhileIFSread-r-dfile;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$tmpdone(find$root-typef -print0)awk-F\tNR1 NF3 {ok1} END {exit !ok}$tmpchmod0644$tmpmv-f--$tmp$outputtrap- EXIT HUP INTTERMlog infomanifest$output用临时夹具做黑盒测试不依赖别篇产物。测试既验证成功内容也验证错误输入的非零退出真实仓库应把被测脚本路径作为参数传入。set-euopipefailfixture$(mktemp-d)traprm -rf -- $fixtureEXITmkdir-p$fixture/input/subprintfalpha\n$fixture/input/a file.txtprintfbeta\n$fixture/input/sub/b.txtscript${1:?请传入清单脚本路径}bash$script$fixture/input$fixture/manifest.tsvtest-s$fixture/manifest.tsvtest$(wc-l$fixture/manifest.tsv)-eq3grep-F$size\tpath$fixture/manifest.tsv/dev/nullifbash$script$fixture/missing$fixture/bad.tsv;thenecho错误输入不应成功2exit1fiprintfall 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 ManualBourne Shell BuiltinsShellCheck WikiGNU Coreutilsmktemp 觉得有用就点个赞 收藏方便回头查阅有疑问直接在评论区留言我看到都会回。 本文属于《终端与命令行进阶》系列持续更新关注不迷路。 文章里的代码都能直接跑。想要可直接 clone 的完整工程 配套部署脚本 / 踩坑清单评论一声或发邮件到cj2664qq.com我免费发你。如果你正好在做类似系统、或有工程化难题想找人做也欢迎邮件聊一句——我按实际情况评估能落地的就接单或出方案。评论和邮件都能直接找到我不用跳别的平台。