模块S14 工程化与综合实战篇号S14-01 / 42预计阅读45 分钟主线Bash文章目录本篇目标30 秒速览正文1. 风格约定解决什么问题2. 脚本文件与头部3. 命名约定3.1 变量3.2 函数3.3 退出码团队内约定4. 引用约定5. 函数与文件布局6. 错误处理约定6.1 三层模型6.2 die 与 usage6.3 return 与 exit6.4 与 set -e 协作7. 标准输出与标准错误8. 注释写什么9. 与 S13 可移植性的关系10. 风格约定速查表读脚本检查清单练习判断题改错题实操题下一篇预告本篇目标把前面各模块的零散习惯收成一套工程化风格约定命名、引用、函数布局、错误与退出码。能按约定写出可读、可 review 的 Bash 脚本并在读他人代码时快速识别main/die/usage、$、local等模式。可移植写法见S13本篇侧重团队内 Bash 脚本的一致风格。30 秒速览文件#!/usr/bin/env bashset -euo pipefail业务放main $。命名常量/环境UPPER_SNAKE局部变量lower_snake函数verb_noun或lower_snake。引用变量、路径、$一律双引号数组用${arr[]}S02-01。函数上部定义工具函数usage→exit 2、die→exit 1函数内临时量用localS08-02。错误预期失败用return致命错误die关键步骤cmd || die或依赖-e。日志用户可见信息stdout错误与诊断stderr。正文1. 风格约定解决什么问题语法正确 ≠ 好维护。多人协作或半年后自己重读时差异来自问题无约定时变量是否已定义猜、搜全文引号是否漏写隐蔽词拆分 bug失败是否退出set -e与手写检查混用入口在哪从第 1 行读到尾风格约定不是语法课而是读写的共同语言——与S13 可移植性并列S13 管「能不能在 dash 跑」S14 管「Bash 项目里怎么写才一致」。2. 脚本文件与头部#!/usr/bin/env bash# deploy.sh — 发布静态资源到目标目录一行用途说明set-euopipefail项约定shebang#!/usr/bin/env bash专栏默认S13-01第二段可选脚本名 一行用途维护者第一眼set紧跟 shebang/注释后set -euo pipefailS01-04扩展选项团队统一再写如shopt -s inherit_errexit勿个人发挥文件名小写 连字符或下划线见名知意backup-db.sh、lib/common.sh。避免test.sh、run.sh等泛名。3. 命名约定3.1 变量类型风格示例环境变量 / 全局常量全大写readonly DEPLOY_ROOT/opt/app脚本内局部 / 配置小写 下划线target_dir、retry_count循环临时短名可接受i、f作用域很小时「内部用」前缀前导_可选_tmp_file示意非公开 APIreadonlyDEFAULT_PORT8080log_level${LOG_LEVEL:-info}main(){localsrc_dir$1localdest_dir$2...}readonly脚本级常量防止误改。local函数内变量S08-02避免污染全局命名空间。3.2 函数风格示例说明动词_名词require_file、copy_tree一眼知动作短工具名die、usage、log项目内统一即可避免ls、test与外部命令同名S08-01读脚本见到require_*、ensure_*、validate_*多为前置检查do_*、run_*多为业务步骤。3.3 退出码团队内约定码常见含义0成功1一般错误die、业务失败2用法错误usage、参数不对3125项目自定义文档写明与S04-01一致不要用exit 255等易与信号混淆的值。4. 引用约定默认规则凡变量展开、位置参数、含空格或通配符风险的字符串 →双引号。# 推荐cp$src$destrm-f--$maybe_dash_nameforfin$;doprocess$fdone# 数组S03-04files(a.log b.log)printf%s\n${files[]}写法说明$var防止词拆分与意外通配符S02-01、S02-03$保留每个参数边界优于$*${var:-default}可选配置S03-02:${VAR:?message}必填变量S11-02...固定字面量、正则、不展开的内容刻意不加引号的少数情况算术$(( ))、[[ ... ]]右侧模式、for i in 1 2 3等已知安全列表——写的时候加注释说明为何安全。反模式读脚本时标红rm$file# 空变量 → rm 无参多词 → 多文件ssh$hostcmd# host 含空格即断5. 函数与文件布局推荐自上而下四段与 S01-04、S08-01 骨架一致#!/usr/bin/env bashset-euopipefail# --- 1. 常量 / 默认配置 ---readonlySCRIPT_NAME${0##*/}# --- 2. 工具函数无业务副作用或仅通用副作用---log(){printf[%s] %s\n$(date%H:%M:%S)$*2;}die(){printferror: %s\n$*2;exit1;}usage(){printfusage: %s [-v] dir\n$SCRIPT_NAME2;exit2;}require_cmd(){command-v$1/dev/null21||diecommand not found:$1}# --- 3. 业务函数 ---deploy_dir(){localdir$1[[-d$dir]]||dienot a directory:$dir...}# --- 4. main 入口 ---main(){localverbose0localtargetwhilegetopts:vopt;docase$optinv)verbose1;;*)usage;;esacdoneshift$((OPTIND-1))(($#1))||usagetarget$1require_cmdrsyncdeploy_dir$target}main$段作用常量readonly、默认路径工具函数die、usage、log、require_*业务函数可单测、可读的步骤main $唯一顶层执行流定义顺序函数必须在调用前定义main $在文件末尾。6. 错误处理约定6.1 三层模型层手段场景预防set -u、: ${VAR:?}、require_cmd启动前挡住坏环境检测[[ -f ]]、cmd、管道 pipefail步骤中判断报告die、usage、log→ stderr人看得懂为何停6.2die与usagedie(){printferror: %s\n$*2exit1}usage(){printfusage: %s [-f] file\n${0##*/}2exit2}usage参数解析失败、-h以外的非法选项 →exit 2。die业务/环境错误 →exit 1或团队统一的其他非 0 码。6.3return与exit# 函数内可恢复 / 可跳过is_stale(){[[-n${1:-}]]||return1...}# 函数内致命结束整个脚本fatal_in_main_context(){diecannot continue# die 内部 exit}# 库函数被 source 时慎用 exitS08-03lib_fn(){printfbad\n2return1# 让调用方决定}6.4 与set -e协作set-euopipefail# 显式忽略要有理由greppatternfile||true# 显式失败信息cp$a$b||diecopy failed:$a-$b# 函数里希望「失败但不退出脚本」process_one(){convert$1||return1}读脚本混用|| true、|| die、裸命令时先确认是否开了-eS01-04、S08-01 §9。7. 标准输出与标准错误流用途stdout管道/重定向要消费的数据json、path列表stderr日志、进度、错误log、die、usagelog(){printf[info] %s\n$*2;}result_path$(find_upward$start)# find_upward 只 echo 路径到 stdout反模式echo debug: ...混在会进管道的命令之间污染下游S09-01。8. 注释写什么该写不必写非显而易见的业务规则i$((i1)) # 加 1外部命令的平台假设复述代码字面意思退出码、环境变量契约# GNU date only; BSD use gdate (documented in README)expire$(date-d7 days%F)9. 与 S13 可移植性的关系场景选择团队 CI、运维机均为 Bash本篇约定 #!/usr/bin/env bash必须/bin/sh改用S13-02printf、[、无local开源项目README 写明 Bash 版本下限不要shebang 写sh却用本篇 Bash 风格——这是 S13-01 红旗。10. 风格约定速查表主题推荐shebang#!/usr/bin/env bash选项set -euo pipefail入口main $常量readonly UPPER_SNAKE函数内变量local lower_snake参数$、$1用法错误usage→2运行错误die→1依赖command -vS13-02日志2读脚本检查清单第一行 shebang 与文内语法是否一致Bash vs sh是否有set -euo pipefail或团队等价物变量、$是否普遍加双引号是否有main $或清晰的单一入口usage/die/log命名与退出码是否一致函数内是否用local有无全局污染错误信息是否走stderr数据是否走stdout|| true是否都有理由不是掩盖真 bug练习判断题脚本内配置常量推荐readonly MAX_RETRIES3形式。$在传递参数时通常优于$*。usage函数应惯例以退出码 2 结束脚本。所有echo都应改为写到 stdout包括错误提示。函数内的循环变量i应用local i声明若函数可能被递归或重入。参考答案对。对。对团队可自定义但 2 是常见约定。错错误与日志应stderr如2。对良好习惯避免泄漏到全局。改错题指出下面片段中风格/健壮性问题至少 4 处#!/usr/bin/env bashset-emax3deploy(){src$1dest$2cp$src$dest}if[$#-lt1];thenechousage:$0direxit0fideploy$1/var/www参考答案示例答案不唯一缺-u、-o pipefail与专栏骨架不一致。max若不变可readonly MAX3函数内src/dest应local。cp $src $dest应cp $src $dest。用法错误却exit 0应exit 2且输出到stderr。无main $结构参数未引号传递deploy $1 /var/www。echo usage宜改为usage函数或printf usage: ...\n $0 2。实操题按本篇约定为下面需求写头部 工具函数 main 骨架业务体可...脚本名sync-site.sh必须恰好 1 个参数目标目录依赖rsync不存在则die支持-ndry-run传给rsync -n参考答案#!/usr/bin/env bash# sync-site.sh — 同步站点目录到目标路径set-euopipefailreadonlySCRIPT_NAME${0##*/}die(){printferror: %s\n$*2;exit1;}usage(){printfusage: %s [-n] dest-dir\n$SCRIPT_NAME2;exit2;}require_cmd(){command-v$1/dev/null21||diecommand not found:$1}main(){localdry_run0localdestwhilegetopts:nopt;docase$optinn)dry_run1;;*)usage;;esacdoneshift$((OPTIND-1))(($#1))||usagedest$1require_cmdrsyncif((dry_run));thenrsync-an./site/$dest/elsersync-a./site/$dest/fi}main$下一篇预告S14-02《脚本模板从 20 行工具到可维护项目》— 在风格约定之上给出可复制目录结构与多文件协作模板。
Shell脚本精读 · S14-01 | 风格约定:命名、引用、函数与错误处理
模块S14 工程化与综合实战篇号S14-01 / 42预计阅读45 分钟主线Bash文章目录本篇目标30 秒速览正文1. 风格约定解决什么问题2. 脚本文件与头部3. 命名约定3.1 变量3.2 函数3.3 退出码团队内约定4. 引用约定5. 函数与文件布局6. 错误处理约定6.1 三层模型6.2 die 与 usage6.3 return 与 exit6.4 与 set -e 协作7. 标准输出与标准错误8. 注释写什么9. 与 S13 可移植性的关系10. 风格约定速查表读脚本检查清单练习判断题改错题实操题下一篇预告本篇目标把前面各模块的零散习惯收成一套工程化风格约定命名、引用、函数布局、错误与退出码。能按约定写出可读、可 review 的 Bash 脚本并在读他人代码时快速识别main/die/usage、$、local等模式。可移植写法见S13本篇侧重团队内 Bash 脚本的一致风格。30 秒速览文件#!/usr/bin/env bashset -euo pipefail业务放main $。命名常量/环境UPPER_SNAKE局部变量lower_snake函数verb_noun或lower_snake。引用变量、路径、$一律双引号数组用${arr[]}S02-01。函数上部定义工具函数usage→exit 2、die→exit 1函数内临时量用localS08-02。错误预期失败用return致命错误die关键步骤cmd || die或依赖-e。日志用户可见信息stdout错误与诊断stderr。正文1. 风格约定解决什么问题语法正确 ≠ 好维护。多人协作或半年后自己重读时差异来自问题无约定时变量是否已定义猜、搜全文引号是否漏写隐蔽词拆分 bug失败是否退出set -e与手写检查混用入口在哪从第 1 行读到尾风格约定不是语法课而是读写的共同语言——与S13 可移植性并列S13 管「能不能在 dash 跑」S14 管「Bash 项目里怎么写才一致」。2. 脚本文件与头部#!/usr/bin/env bash# deploy.sh — 发布静态资源到目标目录一行用途说明set-euopipefail项约定shebang#!/usr/bin/env bash专栏默认S13-01第二段可选脚本名 一行用途维护者第一眼set紧跟 shebang/注释后set -euo pipefailS01-04扩展选项团队统一再写如shopt -s inherit_errexit勿个人发挥文件名小写 连字符或下划线见名知意backup-db.sh、lib/common.sh。避免test.sh、run.sh等泛名。3. 命名约定3.1 变量类型风格示例环境变量 / 全局常量全大写readonly DEPLOY_ROOT/opt/app脚本内局部 / 配置小写 下划线target_dir、retry_count循环临时短名可接受i、f作用域很小时「内部用」前缀前导_可选_tmp_file示意非公开 APIreadonlyDEFAULT_PORT8080log_level${LOG_LEVEL:-info}main(){localsrc_dir$1localdest_dir$2...}readonly脚本级常量防止误改。local函数内变量S08-02避免污染全局命名空间。3.2 函数风格示例说明动词_名词require_file、copy_tree一眼知动作短工具名die、usage、log项目内统一即可避免ls、test与外部命令同名S08-01读脚本见到require_*、ensure_*、validate_*多为前置检查do_*、run_*多为业务步骤。3.3 退出码团队内约定码常见含义0成功1一般错误die、业务失败2用法错误usage、参数不对3125项目自定义文档写明与S04-01一致不要用exit 255等易与信号混淆的值。4. 引用约定默认规则凡变量展开、位置参数、含空格或通配符风险的字符串 →双引号。# 推荐cp$src$destrm-f--$maybe_dash_nameforfin$;doprocess$fdone# 数组S03-04files(a.log b.log)printf%s\n${files[]}写法说明$var防止词拆分与意外通配符S02-01、S02-03$保留每个参数边界优于$*${var:-default}可选配置S03-02:${VAR:?message}必填变量S11-02...固定字面量、正则、不展开的内容刻意不加引号的少数情况算术$(( ))、[[ ... ]]右侧模式、for i in 1 2 3等已知安全列表——写的时候加注释说明为何安全。反模式读脚本时标红rm$file# 空变量 → rm 无参多词 → 多文件ssh$hostcmd# host 含空格即断5. 函数与文件布局推荐自上而下四段与 S01-04、S08-01 骨架一致#!/usr/bin/env bashset-euopipefail# --- 1. 常量 / 默认配置 ---readonlySCRIPT_NAME${0##*/}# --- 2. 工具函数无业务副作用或仅通用副作用---log(){printf[%s] %s\n$(date%H:%M:%S)$*2;}die(){printferror: %s\n$*2;exit1;}usage(){printfusage: %s [-v] dir\n$SCRIPT_NAME2;exit2;}require_cmd(){command-v$1/dev/null21||diecommand not found:$1}# --- 3. 业务函数 ---deploy_dir(){localdir$1[[-d$dir]]||dienot a directory:$dir...}# --- 4. main 入口 ---main(){localverbose0localtargetwhilegetopts:vopt;docase$optinv)verbose1;;*)usage;;esacdoneshift$((OPTIND-1))(($#1))||usagetarget$1require_cmdrsyncdeploy_dir$target}main$段作用常量readonly、默认路径工具函数die、usage、log、require_*业务函数可单测、可读的步骤main $唯一顶层执行流定义顺序函数必须在调用前定义main $在文件末尾。6. 错误处理约定6.1 三层模型层手段场景预防set -u、: ${VAR:?}、require_cmd启动前挡住坏环境检测[[ -f ]]、cmd、管道 pipefail步骤中判断报告die、usage、log→ stderr人看得懂为何停6.2die与usagedie(){printferror: %s\n$*2exit1}usage(){printfusage: %s [-f] file\n${0##*/}2exit2}usage参数解析失败、-h以外的非法选项 →exit 2。die业务/环境错误 →exit 1或团队统一的其他非 0 码。6.3return与exit# 函数内可恢复 / 可跳过is_stale(){[[-n${1:-}]]||return1...}# 函数内致命结束整个脚本fatal_in_main_context(){diecannot continue# die 内部 exit}# 库函数被 source 时慎用 exitS08-03lib_fn(){printfbad\n2return1# 让调用方决定}6.4 与set -e协作set-euopipefail# 显式忽略要有理由greppatternfile||true# 显式失败信息cp$a$b||diecopy failed:$a-$b# 函数里希望「失败但不退出脚本」process_one(){convert$1||return1}读脚本混用|| true、|| die、裸命令时先确认是否开了-eS01-04、S08-01 §9。7. 标准输出与标准错误流用途stdout管道/重定向要消费的数据json、path列表stderr日志、进度、错误log、die、usagelog(){printf[info] %s\n$*2;}result_path$(find_upward$start)# find_upward 只 echo 路径到 stdout反模式echo debug: ...混在会进管道的命令之间污染下游S09-01。8. 注释写什么该写不必写非显而易见的业务规则i$((i1)) # 加 1外部命令的平台假设复述代码字面意思退出码、环境变量契约# GNU date only; BSD use gdate (documented in README)expire$(date-d7 days%F)9. 与 S13 可移植性的关系场景选择团队 CI、运维机均为 Bash本篇约定 #!/usr/bin/env bash必须/bin/sh改用S13-02printf、[、无local开源项目README 写明 Bash 版本下限不要shebang 写sh却用本篇 Bash 风格——这是 S13-01 红旗。10. 风格约定速查表主题推荐shebang#!/usr/bin/env bash选项set -euo pipefail入口main $常量readonly UPPER_SNAKE函数内变量local lower_snake参数$、$1用法错误usage→2运行错误die→1依赖command -vS13-02日志2读脚本检查清单第一行 shebang 与文内语法是否一致Bash vs sh是否有set -euo pipefail或团队等价物变量、$是否普遍加双引号是否有main $或清晰的单一入口usage/die/log命名与退出码是否一致函数内是否用local有无全局污染错误信息是否走stderr数据是否走stdout|| true是否都有理由不是掩盖真 bug练习判断题脚本内配置常量推荐readonly MAX_RETRIES3形式。$在传递参数时通常优于$*。usage函数应惯例以退出码 2 结束脚本。所有echo都应改为写到 stdout包括错误提示。函数内的循环变量i应用local i声明若函数可能被递归或重入。参考答案对。对。对团队可自定义但 2 是常见约定。错错误与日志应stderr如2。对良好习惯避免泄漏到全局。改错题指出下面片段中风格/健壮性问题至少 4 处#!/usr/bin/env bashset-emax3deploy(){src$1dest$2cp$src$dest}if[$#-lt1];thenechousage:$0direxit0fideploy$1/var/www参考答案示例答案不唯一缺-u、-o pipefail与专栏骨架不一致。max若不变可readonly MAX3函数内src/dest应local。cp $src $dest应cp $src $dest。用法错误却exit 0应exit 2且输出到stderr。无main $结构参数未引号传递deploy $1 /var/www。echo usage宜改为usage函数或printf usage: ...\n $0 2。实操题按本篇约定为下面需求写头部 工具函数 main 骨架业务体可...脚本名sync-site.sh必须恰好 1 个参数目标目录依赖rsync不存在则die支持-ndry-run传给rsync -n参考答案#!/usr/bin/env bash# sync-site.sh — 同步站点目录到目标路径set-euopipefailreadonlySCRIPT_NAME${0##*/}die(){printferror: %s\n$*2;exit1;}usage(){printfusage: %s [-n] dest-dir\n$SCRIPT_NAME2;exit2;}require_cmd(){command-v$1/dev/null21||diecommand not found:$1}main(){localdry_run0localdestwhilegetopts:nopt;docase$optinn)dry_run1;;*)usage;;esacdoneshift$((OPTIND-1))(($#1))||usagedest$1require_cmdrsyncif((dry_run));thenrsync-an./site/$dest/elsersync-a./site/$dest/fi}main$下一篇预告S14-02《脚本模板从 20 行工具到可维护项目》— 在风格约定之上给出可复制目录结构与多文件协作模板。