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` 与 `usage`
- 6.3 `return` 与 `exit`
- 6.4 与 `set -e` 协作
- 7. 标准输出与标准错误
- 8. 注释写什么
- 9. 与 S13 可移植性的关系
- 10. 风格约定速查表
- 读脚本检查清单
- 练习
- 判断题
- 改错题
- 实操题
- 下一篇预告
本篇目标
把前面各模块的零散习惯收成一套工程化风格约定:命名、引用、函数布局、错误与退出码。能按约定写出可读、可 review 的 Bash 脚本,并在读他人代码时快速识别main/die/usage、"$@"、local等模式。可移植写法见S13;本篇侧重团队内 Bash 脚本的一致风格。
30 秒速览
- 文件:
#!/usr/bin/env bash+set -euo pipefail;业务放main "$@"。 - 命名:常量/环境
UPPER_SNAKE;局部变量lower_snake;函数verb_noun或lower_snake。 - 引用:变量、路径、
"$@"一律双引号;数组用"${arr[@]}"(S02-01)。 - 函数:上部定义工具函数;
usage→exit 2、die→exit 1;函数内临时量用local(S08-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 pipefail(S01-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(示意非公开 API) |
readonlyDEFAULT_PORT=8080log_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、参数不对) |
| 3~125 | 项目自定义(文档写明) |
与S04-01一致;不要用exit 255等易与信号混淆的值。
4. 引用约定
默认规则:凡变量展开、位置参数、含空格或通配符风险的字符串 →双引号。
# 推荐cp"$src""$dest"rm-f--"$maybe_dash_name"forfin"$@";doprocess"$f"done# 数组(S03-04)files=(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$host"cmd"# 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(){printf'error: %s\n'"$*">&2;exit1;}usage(){printf'usage: %s [-v] <dir>\n'"$SCRIPT_NAME">&2;exit2;}require_cmd(){command-v"$1">/dev/null2>&1||die"command not found:$1"}# --- 3. 业务函数 ---deploy_dir(){localdir=$1[[-d"$dir"]]||die"not a directory:$dir"...}# --- 4. main 入口 ---main(){localverbose=0localtargetwhilegetopts':v'opt;docase$optinv)verbose=1;;*)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与usage
die(){printf'error: %s\n'"$*">&2exit1}usage(){printf'usage: %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(){die"cannot continue"# die 内部 exit}# 库函数被 source 时:慎用 exit(S08-03)lib_fn(){printf'bad\n'>&2return1# 让调用方决定}6.4 与set -e协作
set-euopipefail# 显式忽略(要有理由)greppatternfile||true# 显式失败信息cp"$a""$b"||die"copy failed:$a->$b"# 函数里希望「失败但不退出脚本」process_one(){convert"$1"||return1}读脚本:混用|| true、|| die、裸命令时,先确认是否开了-e(S01-04、S08-01 §9)。
7. 标准输出与标准错误
| 流 | 用途 |
|---|---|
| stdout | 管道/重定向要消费的数据(json、path列表) |
| stderr | 日志、进度、错误(log、die、usage) |
log(){printf'[info] %s\n'"$*">&2;}result_path=$(find_upward"$start")# find_upward 只 echo 路径到 stdout反模式:echo "debug: ..."混在会进管道的命令之间,污染下游(S09-01)。
8. 注释写什么
| 该写 | 不必写 |
|---|---|
| 非显而易见的业务规则 | i=$((i+1)) # 加 1 |
| 外部命令的平台假设 | 复述代码字面意思 |
| ` | |
| 退出码、环境变量契约 |
# GNU date only; BSD use gdate (documented in README)expire=$(date-d'+7 days'+%F)9. 与 S13 可移植性的关系
| 场景 | 选择 |
|---|---|
| 团队 CI、运维机均为 Bash | 本篇约定 +#!/usr/bin/env bash |
必须/bin/sh | 改用S13-02(printf、[、无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 -v(S13-02) |
| 日志 | >&2 |
读脚本检查清单
- 第一行 shebang 与文内语法是否一致(Bash vs sh)
- 是否有
set -euo pipefail(或团队等价物) - 变量、
"$@"是否普遍加双引号 - 是否有
main "$@"或清晰的单一入口 usage/die/log命名与退出码是否一致- 函数内是否用
local,有无全局污染 - 错误信息是否走stderr,数据是否走stdout
|| true是否都有理由(不是掩盖真 bug)
练习
判断题
- 脚本内配置常量推荐
readonly MAX_RETRIES=3形式。 "$@"在传递参数时通常优于$*。usage函数应惯例以退出码 2 结束脚本。- 所有
echo都应改为写到 stdout,包括错误提示。 - 函数内的循环变量
i应用local i声明(若函数可能被递归或重入)。
- 对。
- 对。
- 对(团队可自定义,但 2 是常见约定)。
- 错(错误与日志应stderr,如
>&2)。 - 对(良好习惯,避免泄漏到全局)。
改错题
指出下面片段中风格/健壮性问题(至少 4 处):
#!/usr/bin/env bashset-emax=3deploy(){src=$1dest=$2cp$src$dest}if[$#-lt1];thenechousage:$0direxit0fideploy$1/var/www参考答案示例答案(不唯一):
- 缺
-u、-o pipefail(与专栏骨架不一致)。 max若不变可readonly MAX=3;函数内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(){printf'error: %s\n'"$*">&2;exit1;}usage(){printf'usage: %s [-n] <dest-dir>\n'"$SCRIPT_NAME">&2;exit2;}require_cmd(){command-v"$1">/dev/null2>&1||die"command not found:$1"}main(){localdry_run=0localdestwhilegetopts':n'opt;docase$optinn)dry_run=1;;*)usage;;esacdoneshift$((OPTIND-1))(($#==1))||usagedest=$1require_cmdrsyncif((dry_run));thenrsync-an./site/"$dest/"elsersync-a./site/"$dest/"fi}main"$@"下一篇预告
S14-02:《脚本模板:从 20 行工具到可维护项目》— 在风格约定之上,给出可复制目录结构与多文件协作模板。