统一示例项目(sample-project)
全知识库共享这一套可运行的最小项目,用于演示脚本结构、参数校验、错误处理、trap 清理、 标准输出与标准错误分离、无框架测试与 bats 测试,以及 POSIX 可移植性验证。各章节示例会复用其中的 函数库与约定,避免每篇重复搭建脚手架。
一、目录结构
text
sample-project/
├── bin/
│ ├── greet.sh # POSIX Shell 命令行入口(问候语生成器)
│ ├── stats.sh # Bash 专用入口(从 stdin 统计整数)
│ ├── log-summary.sh # POSIX 入口(校验并聚合 TSV 日志)
│ └── retry.sh # POSIX 入口(有限重试、退避与抖动)
├── lib/
│ └── common.sh # 可复用函数库(日志、die、依赖检查、整数校验)
├── test/
│ ├── run-tests.sh # 无框架测试入口(POSIX sh)
│ ├── greet.bats # bats-core 测试示例
│ └── fixtures/
│ └── flaky # 临时 PATH 命令桩
└── index.md # 本文件二、设计要点
| 约定 | 说明 | 对应章节 |
|---|---|---|
| 数据走 stdout,日志走 stderr | 让管道与断言只处理“数据”,日志不污染结果 | 08 |
set -eu(POSIX)/ set -euo pipefail(Bash) | 未定义变量即报错;Bash 额外让管道失败可见 | 16 |
trap ... EXIT INT TERM HUP | 正常退出、错误退出、信号退出都清理临时目录 | 10 |
mktemp -d 建隔离目录 | 不用可预测的 /tmp/name.$$,避免竞态与冲突 | 19 |
getopts 解析选项 | 标准选项解析,配合 --/OPTIND | 05 |
while IFS= read -r 读输入 | 不用 for x in $(...) 读任意文本 | 13 |
${name} 花括号定界 | 规避旧 Bash 在 UTF-8 下变量名边界解析问题(见下方“踩坑”) | 04 |
三、运行方法
sh
# 1) POSIX 入口
sh bin/greet.sh 世界 # 输出:你好,世界!
sh bin/greet.sh -n 2 -g Hello 世界 # 输出两行 Hello,世界!
# 2) Bash 入口(从 stdin 读整数)
printf '%s\n' 3 1 4 1 5 | bash bin/stats.sh # count=5 sum=14 max=5
# 3) 日志汇总与有限重试
sh bin/log-summary.sh access.tsv
RETRY_JITTER_MAX=0 sh bin/retry.sh 3 0 command arg...
# 4) 无框架测试
sh test/run-tests.sh
# 5) bats 测试(需安装 bats-core ≥ 1.5)
bats test/greet.bats四、验证记录(核验日期 2026-09-07)
验证环境:macOS 26.6.2(25G83,arm64),Locale zh_CN.UTF-8。
1、静态检查
text
sh -n / bash -n 全部通过
shellcheck 0.11.0 --shell=sh / --shell=bash 全部通过(0 warning)
shfmt 3.14.1 -i 4 -ci 规范,diff 为空2、测试执行
text
sh test/run-tests.sh 合计:通过 11,失败 0(exit=0)
bats test/greet.bats 1..6 全部 ok(exit=0)3、POSIX 可移植性矩阵(greet.sh)
| 解释器 | 版本 | 结果 | 退出码 |
|---|---|---|---|
| dash | macOS /bin/dash | 你好,世界! | 0 |
| bash --posix | 5.3.15 | 你好,世界! | 0 |
| ksh | 93u+ 2012-08-01 | 你好,世界! | 0 |
| sh(Bash 3.2.57) | macOS /bin/sh | 你好,世界! | 0 |
| bash | 5.3.15 | 你好,世界! | 0 |
stats.sh使用数组与pipefail,仅在 Bash 上验证(3.2.57 与 5.3.15),不纳入 POSIX 矩阵。
五、踩坑指南(本项目实测)
坑 1:旧 Bash(3.2)在 UTF-8 下把全角标点并入变量名
初版写 "...(name=$name)",在 macOS /bin/sh(Bash 3.2.57)下报 name�: unbound variable,因为它把紧跟 $name 的全角 ) 的首字节当成了变量名的一部分。
- 治标:用
${name}显式定界变量名,让解析器不再吞掉后续字节。 - 治本:需要精确文本时优先
printf并把变量放进独立参数,而非拼进带 CJK 的字符串字面量。
坑 2:bats 默认把 stderr 合并进 $output
本项目日志写 stderr,若直接 run 再断言 $output 会因混入日志而失败。 用 run --separate-stderr(bats ≥ 1.5)让 $output 只含 stdout,并在 setup 中声明 bats_require_minimum_version 1.5.0 消除 BW02 警告。
坑 3:重试测试不能依赖真实网络和真实等待
retry.sh 的测试通过临时 PATH 注入 test/fixtures/flaky:前两次固定返回 75,第三次成功; 同时设置 RETRY_JITTER_MAX=0、基础延迟为 0。这样测试能精确断言调用次数,且不受网络、随机数和时间影响。
六、关联章节
检索关键词
sample-project、Shell 示例项目、trap 清理、getopts、mktemp、bats、shellcheck、shfmt、可移植性矩阵。
