ShellCheck 到底比 bash -n 多查了什么:三条高频规则与 CI 落地
和 bash -n 的区别
ShellCheck 更接近编译器的前端:先完整解析脚本生成抽象语法树,再跟踪变量的定义与使用,理解管道、命令替换、子 shell、条件分支之间的控制流,最后套用内置规则库。规则库里每一条基本都对应一个真实发生过的事故。
bash -n 只做语法检查,不跟踪变量数据流。脚本能通过 bash -n,不代表变量没拼错、引号没漏。
三条高频规则
SC2086:变量不加引号会触发分词
$backup_dir 含空格时会被拆成多个参数,命令变形;变量为空时更糟,参数直接消失。
rm -rf $backup_dir # 含空格 → 拆成多个参数
rm -rf "$backup_dir" # 加引号
SC2164:cd 的退出码没被处理
发布脚本里的经典坑:先 cd 到发布目录再构建,cd 失败时脚本继续往下跑,后面所有相对路径都落到错误目录。
cd "$dir" || exit 1
SC2155:声明与赋值写在同一行
local var="$(command)" 会把命令替换的退出状态吞掉——local 自身返回 0,把失败退出码盖过去。声明和赋值要拆开:
local var
var="$(command)"
其它高频规则
- SC2154:引用未定义或拼错的变量
- SC2034:声明了但未使用的变量
- SC2046:命令替换结果的分词错误
- SC2206:用字符串存列表再展开,本身脆弱
集成方式与落地坑
VSCode 的 shellcheck 插件、pre-commit、CI 都可以直接调用 shellcheck 命令。
坑一:一次性对全仓库开启。 若仓库已积压大量历史脚本,先让「新改动和本次修改的文件」必须通过,存量脚本单独建待处理清单。常见失败路径是 CI 一次开最严规则,几十个脚本全红,流水线连红两周,最后把检查从 CI 摘掉。
坑二:不加判断地 disable。 某些规则确实会因脚本的动态特性误报,但加 # shellcheck disable= 前先弄清触发原因。真实反例:有人把变量名拼错,警告出现后没看文档,顺手加了 SC2154 的 disable,错误一路带到生产。
坑三:只检查主脚本、忽略被 source 的公共文件。 ShellCheck 对单文件分析准,但追不到跨文件变量传接。把整个 lib 目录纳入检查,必要时用 --external-sources 显式跟踪外部 source 文件。
坑四:对警告级别一刀切。 不同规则的可信度和误报率差别很大。先从 error 级别入手,warning 级按团队接受度分批开放,并记录哪些规则易误报。
落地建议
- 先在新增脚本和本次改动涉及的脚本上启用,避免历史包袱拖垮节奏。
- 把
shellcheck放进 CI 的 lint 阶段,设「新增脚本必须零 error 警告」的硬门槛。 - 所有 disable 注释必须写明理由,review 时顺着注释判断例外是否值得保留。
- 用
-s参数明确脚本类型,区分 bash 与 POSIX sh,减少平台差异带来的误报。 - 把 ShellCheck 和
set -euo pipefail结合使用:前者解决静态问题,后者提供运行时保护,二者互补。