
模块S14 工程化与综合实战篇号S14-02 / 42预计阅读50 分钟主线Bash文章目录本篇目标30 秒速览正文1. 先选档位再套模板2. 一档模板20 行工具3. 二档模板单文件 CLI4. 三档模板多文件小项目4.1 推荐目录4.2 库文件 lib/common.sh4.3 库文件 lib/deploy_lib.sh4.4 入口 bin/deploy.sh5. 配置与环境变量5.1 config/defaults.env5.2 .env.example5.3 加载顺序约定写进 README6. scripts/env.sh可选给人用7. 何时从一档升到二档、二档升到三档8. README 最小模板9. 质量门禁提交前10. 三档对照总表11. 读项目脚本的顺序预告 S14-03读脚本检查清单练习判断题实操题设计题下一篇预告本篇目标在S14-01 风格约定与S08-03 模块化基础上给出三档可直接复制的脚本模板20 行一次性工具、单文件 CLI、多文件小项目。能判断脚本该停在哪一档、何时拆lib/、如何写README与.env.example并用ShellCheck/bash -n做最低质量门禁。30 秒速览一档工具单文件、无参数或极少参数适合 cron 一次性任务。二档CLI单文件 main/getopts/trap百行内可维护。三档项目bin/lib/ 配置 README多入口共享库。升级信号重复函数、多入口、配置变多、需要单独测试库函数。路径入口用BASH_SOURCE算SCRIPT_DIR不依赖 cwdS08-03。质量提交前shellcheckbash -nS12-02。正文1. 先选档位再套模板档位行数量级典型场景文件数一档·工具2040备份单目录、清理/tmp、发一条通知1二档·CLI80200带-h/-v的部署、批处理、运维命令1三档·项目200多子命令、共享库、团队长期维护多个原则能用一档解决就不要上三档重复第二次再抽函数第二个入口脚本再建lib/。读仓库时先看bin/有几个入口、lib/是否纯定义、README 写 source 还是执行。2. 一档模板20 行工具适合逻辑直线、参数 01 个、无人扩展。#!/usr/bin/env bash# prune-logs.sh — 删除 7 天前的 *.logcron 用set-euopipefailreadonlyDAYS7readonlyLOG_DIR${LOG_DIR:-/var/log/myapp}find$LOG_DIR-maxdepth1-name*.log-mtime$DAYS-print-delete要素说明shebang setS01-04 最小可靠集一行用途cron/同事一眼懂readonly 环境默认LOG_DIR可覆盖不改代码无main太短可不包装超过 ~30 行建议升格二档cron 注意写清路径用绝对路径错误进邮件# crontab 示例03* * * /opt/scripts/prune-logs.sh/var/log/prune.log213. 二档模板单文件 CLI适合需要用法说明、选项、清理trap仍是一个人维护。#!/usr/bin/env bash# backup-dir.sh — 打包目录为 .tar.gzset-euopipefailreadonlySCRIPT_NAME${0##*/}# --- 工具函数 ---die(){printferror: %s\n$*2;exit1;}usage(){printfusage: %s [-k] srcdir\n$SCRIPT_NAME2;exit2;}log(){printf[%s] %s\n$(date%H:%M:%S)$*2;}require_cmd(){command-v$1/dev/null21||diecommand not found:$1}# --- 业务 ---make_archive(){localsrc$1keep$2localout tmp[[-d$src]]||dienot a directory:$srcout$(basename$src).tar.gztmp$(mktemp)traprm -f $tmpEXIT INTTERMlogarchiving$src-$outtar-czf$tmp-C$(dirname$src)$(basename$src)if((keep));then[[-f$out]]cp-a$out${out}.bakfimv$tmp$outtrap- EXIT INTTERMlogdone:$out}# --- 入口 ---main(){localkeep0localsrcwhilegetopts:kopt;docase$optink)keep1;;*)usage;;esacdoneshift$((OPTIND-1))(($#1))||usagesrc$1require_cmdtarmake_archive$src$keep}main$与S14-01对照块内容头部shebang、用途、set -euo pipefail工具die、usage、log、require_cmd业务make_archive可再拆末尾main $选项解析用getoptsS08-04临时文件配trapS11-01。4. 三档模板多文件小项目适合多个子命令、库函数复用、配置外置。4.1 推荐目录myapp/ README.md .env.example # 环境变量示例不含密钥 bin/ deploy.sh # 入口可执行 rollback.sh lib/ common.sh # die、log、require_cmd deploy_lib.sh # 部署相关函数 config/ defaults.env # 非敏感默认可被 .env 覆盖 tests/ smoke.sh # 可选bash -n 干跑目录职责bin/可执行入口短、只组装调用lib/被source函数与常量不顶层exitS08-03config/默认值密钥走环境变量不进仓库tests/烟雾测试、固定样例非必须 junit 级4.2 库文件lib/common.sh# lib/common.sh — 公共库请 source勿直接执行if[[${BASH_SOURCE[0]}${0}]];thenprintfsource this file, do not execute\n2exit1fidie(){printferror: %s\n$*2;exit1;}log(){printf[%s] %s\n$(date%H:%M:%S)$*2;}require_cmd(){command-v$1/dev/null21||diecommand not found:$1}4.3 库文件lib/deploy_lib.sh# lib/deploy_lib.sh_LIB_DIR$(cd $(dirname${BASH_SOURCE[0]})pwd) readonly DEFAULT_REMOTE${DEFAULT_REMOTE:-userhost:/var/www} deploy_sync() { local src$1remote${2:-$DEFAULT_REMOTE}require_cmd rsync rsync -a --delete $src/ $remote/}4.4 入口bin/deploy.sh#!/usr/bin/env bash# bin/deploy.sh — 同步站点到远程set-euopipefailSCRIPT_DIR$(cd $(dirname${BASH_SOURCE[0]})pwd) ROOT_DIR$(cd${SCRIPT_DIR}/..pwd) # 可选加载配置存在才加载 [[ -f ${ROOT_DIR}/config/defaults.env ]] source ${ROOT_DIR}/config/defaults.env [[ -f ${ROOT_DIR}/.env ]] source ${ROOT_DIR}/.env source ${ROOT_DIR}/lib/common.sh source ${ROOT_DIR}/lib/deploy_lib.sh usage() { printf usage: %s [-n] [remote]\n ${0##*/}2exit2}main(){localdry0remotewhilegetopts:nopt;docase$optinn)dry1;;*)usage;;esacdoneshift$((OPTIND-1))remote${1:-$DEFAULT_REMOTE}if((dry));thenlogdry-run: would sync${ROOT_DIR}/site/ -$remotereturn0fideploy_sync${ROOT_DIR}/site$remotelogdeployed to$remote}main$读脚本路径从bin/deploy.sh找ROOT_DIR、source顺序再看lib/deploy_lib.sh里真实rsync参数config/defaults.env里默认主机、路径5. 配置与环境变量5.1config/defaults.env# 非敏感默认提交到 gitDEFAULT_REMOTEuserstaging:/var/wwwRETAIN_DAYS75.2.env.example# 复制为 .env 后填写.env 加入 .gitignoreDEFAULT_REMOTEuserprod:/var/wwwAPI_TOKENreplace_me5.3 加载顺序约定写进 README环境变量已 export .env config/defaults.env 脚本内 readonly 默认入口脚本用存在才 source避免本机无.env时报错[[-f${ROOT_DIR}/.env]]source${ROOT_DIR}/.env必填项在main开头用: ${VAR:?}检查S11-02。6.scripts/env.sh可选给人用与S08-03一致面向交互终端不是每个bin/入口的前置条件。# scripts/env.sh — 文档source scripts/env.shROOT$(cd $(dirname${BASH_SOURCE[0]})/..pwd) export PATH${ROOT}/bin:${PATH} source ${ROOT}/lib/common.sh deploy() { ${ROOT}/bin/deploy.sh $;}开发source scripts/env.sh后敲deployCI/cron直接/path/to/bin/deploy.sh不依赖 env.sh7. 何时从一档升到二档、二档升到三档信号建议超过~80 行且多段逻辑抽函数仍单文件两个以上运维脚本复制粘贴同一函数建lib/common.sh第三个bin/入口三档布局 README需要 mock 配置做多环境config/.env库函数要被「单测」式调用lib/纯函数 tests/smoke.sh不要过早拆文件单文件 150 行但结构清晰仍优于三个空洞文件。8. README 最小模板# myapp scripts Bash 4。运维同步与回滚工具。 ## 快速开始 chmod x bin/deploy.sh cp .env.example .env # 编辑 DEFAULT_REMOTE ./bin/deploy.sh -n # 干跑 ## 命令 | 脚本 | 作用 | |------|------| | bin/deploy.sh | 同步 site/ 到远程 | | bin/rollback.sh | 回滚上一版本 | ## 配置 - config/defaults.env — 默认可提交 - .env — 本机覆盖不提交 ## 检查 shellcheck bin/*.sh lib/*.sh bash -n bin/deploy.sh读陌生项目README 表格往往比翻目录快。9. 质量门禁提交前检查命令说明语法bash -n script.sh不执行只解析静态shellcheck script.shS12-02干跑-n选项或DRY_RUN1不真写远程/不真删选项./script.sh -h用法与exit 2tests/smoke.sh示例#!/usr/bin/env bashset-euopipefailROOT$(cd $(dirname${BASH_SOURCE[0]})/..pwd) for f in ${ROOT}/bin/*.sh ${ROOT}/lib/*.sh; do bash -n $f done shellcheck ${ROOT}/bin/*.sh ${ROOT}/lib/*.sh ${ROOT}/bin/deploy.sh-nprintfsmoke ok\n10. 三档对照总表项一档二档三档文件11bin lib configmain可选必须每个入口一个getopts少见常见各入口自有source无少见必须SCRIPT_DIR常不需要需要时再加入口必算README一行注释简短说明完整ShellCheck建议建议建议 smoke11. 读项目脚本的顺序预告 S14-03打开一个脚本仓库时建议路径README → bin/ 入口列表 → 选一个入口的 source 链 → lib/ → config/S14-03将在一段真实风格的长脚本上把 S01S14 的知识点串起来做逐段拆解专栏必读三星之一。读脚本检查清单判断档位单文件工具 vs 多文件项目入口是否在bin/库是否只被sourcesource路径是否基于BASH_SOURCES08-03配置分层defaults / .env / 环境变量谁先谁后文档写的是执行bin/x.sh还是先 source env.sh是否有-n/dry-run或 smoke 测试一档脚本是否被误扩成无结构大文件该升格练习判断题20 行 cron 脚本也必须拆成lib/才规范。三档项目的lib/common.sh顶层不宜随意exit 1。bin/deploy.sh应使用BASH_SOURCE定位项目根而不是假设用户在项目根执行。.env应提交到 git方便同事共享密钥。第二个入口脚本复制同一套die/log时适合抽到lib/common.sh。参考答案错一档单文件即可过度拆分会增加路径成本。对source 时exit会结束当前 ShellS08-03。对。错.env含密钥应gitignore只提交.env.example。对。实操题在纸上或本地建三档目录写出bin/hello.shsourcelib/common.sh定义main接受一个名字参数打印hello, name到 stdout日志log到 stderr。参考答案lib/common.sh节选if[[${BASH_SOURCE[0]}${0}]];thenexit1;filog(){printf[%s] %s\n$(date%H:%M:%S)$*2;}die(){printferror: %s\n$*2;exit1;}bin/hello.sh#!/usr/bin/env bashset-euopipefailSCRIPT_DIR$(cd $(dirname${BASH_SOURCE[0]})pwd) source ${SCRIPT_DIR}/../lib/common.sh usage() { printf usage: %s name\n ${0##*/}2;exit2;}main(){(($#1))||usage loggreeting$1printfhello, %s\n$1}main$设计题backup.sh80 行内含die、tar打包、find清理即将增加restore.sh也要用die和日志格式。应维持一档、升二档还是升三档目录怎么摆参考答案升三档轻量lib/common.sh # die、log、require_cmd bin/backup.sh bin/restore.sh两入口共享库若restore逻辑很长再加lib/backup_lib.sh。单文件 backup 已 80 行且第二入口在即不必强行塞回一档。下一篇预告S14-03《综合实战拆解一段真实开源/运维脚本》— 专栏必读三星收官篇从README → 入口 → source 链 → 条件/循环/管道逐段读串联 S01S14。