用途
本技能用于编写 Linux 上的防御式 Bash 脚本,目标是产出能通过 shellcheck --enable=all 和 shfmt -d 且零警告的脚本。适用目标环境为 Linux 上的 GNU Bash 4.4+,不包含 macOS/BSD 兼容写法、Windows 路径,也不做仅 POSIX 的限制。
脚本基础
推荐的基础骨架:
#!/usr/bin/env bash
set -Eeuo pipefail
shopt -s inherit_errexit
SCRIPT_DIR=$(dirname -- "${BASH_SOURCE[0]}") || exit "$?"
SCRIPT_DIR=$(cd -- "${SCRIPT_DIR}" && pwd -P) || exit "$?"
readonly SCRIPT_DIR
trap 'printf "Error at %s:%d\n" "${BASH_SOURCE[0]}" "$LINENO" >&2' ERR
trap 'rm -rf -- "${_tmpdir:-}"' EXIT
要点:
-E让 ERR trap 传播进函数。inherit_errexit让 errexit 传播进$()命令替换。- 脚本自身的数据文件应相对
SCRIPT_DIR解析,绝不使用调用者的 cwd 或git rev-parse --show-toplevel。共享 linter 从其他项目的 git hook、cron 任务或包装脚本调用时,cwd 属于别人,调用者相对的规则路径会解析到不存在的文件:规则集加载为空,零违规,退出 0。这是静默空操作而非报错,而在其自身仓库内运行会因错误原因通过。应在一个不属于脚本自身目录树的临时目录中演练一次。 - 临时目录始终在 EXIT trap 中创建:
_tmpdir=$(mktemp -d)。 - 将主体包进
main函数,并对其调用做保护,使直接执行时运行、被 source 时只定义函数以便测试。
核心规则
- 引用展开,命令用数组,绝不把外部数据当作 shell 代码求值。
- 算术前校验数值语法、符号与应用边界。无符号数字先以
10#转换再施加符号;10#-08无效。 - 密钥不得出现在进程参数与跟踪中。通过 stdin 传入,并使用能识别 JSON 的编码器。
- 在“不存在”与“检查失败”有区别处,检查精确退出状态。将
local与readonly声明同命令替换分开。 - 使用 NUL 分隔的文件迭代,校验必需的标志值,拒绝冲突的输出/目标组合。
- 原子替换时在目标旁暂存;多文件激活时切换单一暂存发布引用。
- 保留无关文件,并在清理后报告实际的信号或命令状态。
- 不要假设 Bash 选项在
sh中可用、GNU 工具模式等同于系统调用模式,或配置的回退路径一定可用。
任务专用参考
实现对应行为前先阅读相关参考:
- 命令执行、外部输入、数值转换、参数解析、迭代或子进程状态处理:
input-and-process-safety.md。 - 文件激活、密钥、加锁、重试、清理、权限、日志或可重启自动化:
production-patterns.md。
性能
- 用参数展开替代外部命令:
${path%/*}而非dirname,${path##*/}而非basename,${var//old/new}而非sed。 - 用
(( ))替代expr;用[[ =~ ]]替代echo | grep。 - 缓存结果:
val=$(cmd)一次,复用$val。 - 并行工作用
xargs -0 -P "$(nproc)"。 - 用
declare -A map做查找,替代反复 grep。
Bash 4.4+ / 5.x
${var@Q}输出 shell 引用形式(Bash 4.4+);${var@U}大写、${var@L}小写(Bash 5.1+)。declare -n ref=varname命名引用用于间接访问。wait -n等待任一后台任务。$EPOCHSECONDS、$EPOCHREALTIME:无需 forkdate即可获得时间戳(Bash 5.0+)。
Linux 特有
- GNU coreutils 与 macOS 不同:
sed -i(无''后缀)、grep -P(支持 PCRE)、readlink -f(规范路径)。 timeout 30s cmd防止自动化挂起。sed -i会把符号链接替换为普通文件:它写入临时文件并重命名覆盖该路径,两份副本之后会无报错地分叉。应使用sed -i --follow-symlinks或按名称编辑目标,并在脚本化编辑后检查git status --short中的T(类型变更)。
ShellCheck
运行 shellcheck --enable=all script.sh。关键规则:
- SC2155:声明与赋值分开。
- SC2086:变量加双引号。
- SC2046:命令替换加引号。
- SC2164:
cd dir || exit。 - SC2327/SC2328:保持 stdout 对命令替换可用;将其重定向走会使捕获值为空。
- 从
${BASH_REMATCH[n]}读取正则捕获,而非位置参数。
提交前:shellcheck *.sh && shfmt -i 2 -ci -d *.sh。
验证
宣布完成前运行 shellcheck --enable=all 与 shfmt -d,要求零警告。测试边界情况:空输入、缺失文件、路径含空格。
若 shellcheck 或 shfmt 未安装(command -v shellcheck 失败),则检查是被跳过而非通过:报告“static analysis not run: shellcheck unavailable”,并回退到 bash -n 仅做语法检查。被跳过的 linter 与上文空规则文件一样,是静默空操作。
限制
本技能面向 Linux 上的 GNU Bash 4.4+,不提供 macOS/BSD 兼容写法、Windows 路径或仅 POSIX 限制。其他平台、版本或工具链的适配方式暂未提供。