ShellCheck 不能直接用于 .git/hooks/pre-commit,因默认扫描全目录、忽略暂存区范围、输出格式不兼容退出码逻辑;须用 lefthook 封装或手动提取 staged 文件并加 -f gcc、-s bash、路径过滤及 set -e/u 防错。

ShellCheck 不能直接塞进 .git/hooks/pre-commit 里跑,硬塞会导致路径错、忽略文件、不报错就过、甚至卡死提交流程——必须用中间层封装或现代钩子管理器。
为什么裸写 shellcheck 在 pre-commit 里会失效
Git 的原生 Hook 脚本是 Shell 环境,但 shellcheck 默认扫描当前目录下所有 .sh 文件,而 pre-commit 阶段真正要校验的只有暂存区(staged)里的变更脚本。常见问题包括:
-
shellcheck *.sh会扫到未修改的旧脚本,噪音大、耗时长 - 没过滤
node_modules/、dist/、build/等生成目录,频繁报错 - 错误输出格式默认是 human-readable,和 Git Hook 的退出码逻辑不兼容(非零才拦截,但部分警告不触发退出)
- 没加
-f gcc或--severity=error,导致 warning 不中断提交,失去“拦截”意义
用 lefthook + shellcheck 实现精准拦截
推荐用 lefthook(比 Husky 更轻、纯 Go、无 Node 依赖),它天然支持按文件列表传参、自动跳过非匹配文件、可配置失败阈值:
- 在
.lefthook.yml中定义pre-commit钩子:
pre-commit:
commands:
shellcheck-staged:
files: git diff --cached --name-only --diff-filter=ACM | grep '\.sh$'
glob: "*.sh"
run: shellcheck -f gcc -s bash -e SC2086,SC2046,SC1090,SC2154 {files}-
{files}是 lefthook 自动注入的暂存文件列表,安全可靠 -
-e后跟规则 ID 是必须的:比如SC2086(单词拆分风险)、SC2154(未声明变量)常为误报,但SC2034(未使用变量)、SC2294(数组索引越界)建议保留 - 加
-s bash显式指定 shell 类型,避免#!/bin/sh下误判语法
如果坚持用原生 .git/hooks/pre-commit,怎么写才不翻车
必须手动提取 staged 文件、过滤路径、控制退出逻辑,否则就是自欺欺人:
- 开头加
#!/usr/bin/env bash,别用#!/bin/sh——shellcheck依赖 Bash 特性 - 用
git diff --cached --name-only --diff-filter=ACM -- '*.sh'获取变更脚本,注意单引号包裹'*.sh'防止 shell 展开 - 逐个调用
shellcheck -f gcc -s bash "$file",用|| exit 1确保任一错误即中断 - 务必加
set -e和set -u,防止空文件列表导致后续命令静默跳过 - 排除构建产物目录:在
git diff后 pipegrep -vE '^(node_modules|dist|build)/'
容易被忽略的关键点
很多人以为装上 shellcheck 就万事大吉,其实真正卡住落地的是三件事:
-
shellcheck默认不检查 shebang 行之后的注释块,但实际脚本中大量配置参数藏在注释里(如# shellcheck disable=SC2034),得确保团队约定统一加在文件顶部而非行内 - CI 环境里
shellcheck版本可能比本地低,规则不一致,建议在项目根目录锁死版本:shellcheck --version输出写进.shellcheck-version,Hook 中做校验 - 对
source引入的库文件,shellcheck不会递归分析——如果核心逻辑抽成lib.sh,必须单独把它也加入 staged 检查范围,否则改了库却没报错


















