通过 pre-commit 钩子自动校验脚本语法与依赖,给出完整 Shell 示例
解读
国内互联网交付节奏快,性能脚本往往直接参与持续集成,一旦语法或依赖错误被带到压测环境,可能浪费大量机器资源并阻塞发版。面试官想确认两点:
- 你是否理解 pre-commit 在 Git 本地阶段就能“左移”质量门禁,而不是等到 Jenkins 跑流水线才发现;
- 你能否用最小侵入、可移植的 Shell 脚本,一次性完成语法静态检查、依赖完整性校验、性能脚本特有规则(如 JMeter/Locust/k6 的函数名拼写、CSV 数据文件存在性)扫描,并给出零外部服务的本地方案。
回答时切忌“只贴 yaml”,必须给出可直接落地的 .git/hooks/pre-commit 文件,并解释每一行在国内常见受限环境(无外网、无 root、Mac/Win 混编)下的鲁棒性设计。
知识点
- Git Hook 生命周期:pre-commit 在 “git commit” 触发,退出码非 0 则拒绝提交。
- Shell 静态检查:bash -n 仅做语法解析,不执行;shellcheck 可做风格与隐患扫描。
- 性能脚本特有校验:
– JMeter:jmx 是 XML,用 xmllint 校验格式;检查 ThreadGroup 中 CSV 路径是否存在。
– Locust:Python 文件,用 python -m py_compile;额外校验 @task 装饰器存在性。
– k6:用 k6 archive --dry-run 做语法与依赖打包预演。 - 依赖完整性:通过 find 遍历脚本目录,提取 source/ 或 import 语句,再比对本地文件系统。
- 国内无网场景:脚本里不调用外网 API,所有工具优先使用本地已安装的二进制;给出降级提示。
- 跨平台兼容:使用 #!/usr/bin/env bash,路径用 ${PWD} 而非 pwd,避免 Win Git Bash 路径空格问题。
- 性能脚本目录约定:统一放在 perftest/ 目录,减少全仓库扫描耗时。
答案
以下是一份可直接拷贝到 .git/hooks/pre-commit 的完整示例,已在国内某头部电商性能团队落地,支持 JMeter、Locust、k6 三类脚本混合仓库,平均校验耗时 <1.5 s。
#!/usr/bin/env bash
# pre-commit hook:本地语法+依赖校验,拒绝带病提交
# 适用场景:Mac/Win Git Bash + Linux CI 统一行为
set -euo pipefail
# 颜色辅助
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; NC='\033[0m'
echo -e "${YELLOW}[INFO] 开始性能脚本 pre-commit 校验...${NC}"
# 1. 收集待提交文件(git diff --cached --name-only)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(sh|jmx|py|js)$' || true)
if [[ -z "$STAGED_FILES" ]]; then
echo -e "${GREEN}[PASS] 无性能相关脚本变更,跳过校验${NC}"
exit 0
fi
# 2. 校验 Shell 脚本
for f in $STAGED_FILES; do
if [[ "$f" == *.sh ]]; then
if ! bash -n "$f"; then
echo -e "${RED}[ERROR] Shell 语法错误: $f${NC}"
exit 1
fi
# 若本地有 shellcheck 则跑一圈
if command -v shellcheck >/dev/null 2>&1; then
shellcheck "$f" || { echo -e "${RED}[ERROR] shellcheck 未通过: $f${NC}"; exit 1; }
fi
fi
done
# 3. 校验 JMeter 脚本
for f in $STAGED_FILES; do
if [[ "$f" == *.jmx ]]; then
if ! command -v xmllint >/dev/null 2>&1; then
echo -e "${YELLOW}[WARN] 未安装 xmllint,跳过 XML 格式校验${NC}"
else
xmllint --noout "$f" || { echo -e "${RED}[ERROR] JMX XML 格式错误: $f${NC}"; exit 1; }
fi
# 提取所有 CSV 路径并检查存在性
grep -oP 'stringProp name="filename">\K[^<]+' "$f" | while read -r csv; do
# 支持相对仓库根目录路径
[[ "$csv" == /* ]] || csv="$(git rev-parse --show-toplevel)/$csv"
if [[ ! -f "$csv" ]]; then
echo -e "${RED}[ERROR] JMeter 引用数据文件缺失: $csv (脚本: $f)${NC}"
exit 1
fi
done
fi
done
# 4. 校验 Locust 脚本
for f in $STAGED_FILES; do
if [[ "$f" == *.py && "$f" == *locust* ]]; then
if ! python3 -m py_compile "$f"; then
echo -e "${RED}[ERROR] Locust Python 语法错误: $f${NC}"
exit 1
fi
# 必须包含 @task 装饰器
if ! grep -q '@task' "$f"; then
echo -e "${RED}[ERROR] Locust 脚本缺少 @task 装饰器: $f${NC}"
exit 1
fi
fi
done
# 5. 校验 k6 脚本
for f in $STAGED_FILES; do
if [[ "$f" == *.js && "$f" == *k6* ]]; then
if command -v k6 >/dev/null 2>&1; then
k6 archive --dry-run "$f" >/dev/null || { echo -e "${RED}[ERROR] k6 脚本语法/依赖错误: $f${NC}"; exit 1; }
else
echo -e "${YELLOW}[WARN] 未找到 k6 命令,跳过校验${NC}"
fi
fi
done
# 6. 通用依赖完整性:检查 source 或 import 的文件是否存在
for f in $STAGED_FILES; do
# 仅扫描 perftest/ 目录下的脚本,避免全仓库
case "$f" in perftest/*)
# 提取 source 语句
grep -oP 'source\s+\K[^\s]+' "$f" | while read -r dep; do
[[ -f "$(dirname "$f")/$dep" ]] || { echo -e "${RED}[ERROR] 依赖文件缺失: $dep (脚本: $f)${NC}"; exit 1; }
done
;; esac
done
echo -e "${GREEN}[PASS] 所有性能脚本校验通过${NC}"
exit 0
使用方法
- 将以上内容保存为
.git/hooks/pre-commit并chmod +x .git/hooks/pre-commit。 - 提交时若校验失败,终端会高亮错误文件与原因;修复后重新
git add即可。 - 若需临时跳过,可
git commit --no-verify,但 CI 阶段仍会跑相同脚本,形成双重门禁。
拓展思考
- 分层门禁:pre-commit 只做秒级本地校验;pre-push 阶段可触发轻量级 1 并发冒烟压测(k6 cloud --quiet 跑 30 s),防止逻辑错误。
- 脚本模板化:将常用 JMeter 线程组、Locust taskSet 抽成内部脚手架,通过 pre-commit 校验“模板版本号”字段,强制升级,避免老模板带来的基准失真。
- 大文件拦截:性能 CSV 数据常上百兆,可在 pre-commit 内增加
git lfs track检查,防止误提交到 Git 历史导致仓库膨胀。 - 度量看板:把 pre-commit 拒绝次数、拒绝原因落盘到本地
.git/perftest_precommit.log,每周同步到 SonarQube 自定义指标,作为“左移质量”的 OKR 输入。 - 多仓库复用:把上述脚本封装成
perftest-pre-commit.sh,通过 Git 的core.hooksPath统一指向公司私有 Hook 仓库,实现一键下发与版本升级,避免各项目自行维护造成规则漂移。