ShellCheck 是开源的 Bash 静态分析工具,能在脚本执行之前就揪出引号缺失、变量未加引号、命令注入、数组误用等常见隐患。很多线上故障并非逻辑复杂,而是脚本里一个没引号的变量在特殊文件名下崩掉。本文从安装、告警级别、高频陷阱、CI 集成到 pre-commit 钩子,手把手带你把 ShellCheck 落进日常开发,让 Bash 脚本 Bug 挡在运行之前。
一、为什么 Bash 脚本特别需要静态分析
Bash 是解释执行、弱类型、对空格和引号极度敏感的语言。一个 for f in $files 在含空格的文件名上就会分裂成多个参数,导致删错文件、传错参数。这类问题运行前看不出来,运行时才炸。静态分析的价值,就是在不执行的情况下根据规则推断潜在错误。ShellCheck 内置上千条规则,覆盖 POSIX/sh/bash,是社区公认的事实标准。
二、ShellCheck 安装与快速上手
2.1 各平台安装
# Debian / Ubuntu
sudo apt-get install -y shellcheck
# macOS
brew install shellcheck
# 不想本地装?用容器一键扫描(无需安装)
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable /mnt/deploy.sh
2.2 命令行基本用法
# 扫描单个脚本,输出所有告警
shellcheck deploy.sh
# 只显示 error 和 warning(屏蔽 style/info 噪音)
shellcheck -S warning deploy.sh
# 明确指定 shell 方言,避免误判
shellcheck -s bash deploy.sh
三、读懂 ShellCheck 的告警级别
不同级别代表不同风险,修复优先级也不同。CI 里通常把 error 设为硬性失败,warning 设为提醒。
| 级别 | 含义 | 是否必须修 |
|---|---|---|
| error | 几乎一定会出问题的写法 | 必须 |
| warning | 高风险,多数场景会踩 | 建议修 |
| info | 可疑用法或潜在问题 | 视情况 |
| style | 可读性 / 可维护性建议 | 可选 |
四、5 个高频 Bash 陷阱与 ShellCheck 如何拦截
1. 变量未加引号(SC2086)。 这是最多发的事故源头,空格会让一个变量裂成多个参数。
# 错误:未引号,含空格的文件名会被拆开
files="a b c"
for f in $files; do echo "$f"; done
# 正确:用数组保存,再带引号展开
files=(a b c)
for f in "${files[@]}"; do echo "$f"; done
2. 命令替换未引号(SC2046)。 grep -l xxx $(cat list) 在文件名含空格时同样会裂开,应写成 "$(...)" 或改用 mapfile 读数组。
3. find 拼接参数(SC2010 / SC2038)。 用 -name '*.tmp' 配 xargs 时,含空格文件会出问题,优先 -print0 | xargs -0。
4. set -e 的陷阱(SC2181)。 用 command; if [ $? -eq 0 ] 判断失败,应改为 if command; then ...,否则 set -e 会提前退出。
5. 未使用变量 / 死代码(SC2034 / SC2317)。 函数里写了 exit 却在 if 分支中永远到不了,ShellCheck 会提示,常暴露逻辑错误。
五、在 CI 中集成 ShellCheck
把检查搬进流水线,未通过的 PR 直接变红,从源头卡住坏脚本。下面这段 GitHub Actions 工作流会扫描 ./scripts 下所有 shell 脚本(参考《GitHub Actions 实战》的配置思路)。
name: shellcheck
on: [push, pull_request]
jobs:
sc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
scandir: './scripts'
六、与 SonarQube、Trivy 组成质量防线
单靠一个工具覆盖不全:ShellCheck 管 shell 脚本,SonarQube 管多语言静态分析,Trivy 管容器镜像漏洞扫描。三者分别落在 CI 的不同阶段,形成「代码 → 镜像 → 运行时」的纵深防御,比任何单点检查都更稳。
七、VS Code 实时提示与 pre-commit 钩子
在 VS Code 里装 ShellCheck 扩展,可以边写边标红,把问题消灭在敲键盘的时候。再配一个 pre-commit 钩子,提交前自动扫新增脚本,双重保险:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/shellcheck-py/shellcheck-py
rev: v0.10.0
hooks:
- id: shellcheck
args: ["--severity=warning"]
八、常见误报与精准忽略
有些规则在特定场景不适用,切忌全局关闭,而要用注解精准忽略,既消除噪音又保留其他地方的检查:
# 仅对本条命令禁用 SC2086(确实需要按空格拆分参数)
# shellcheck disable=SC2086
docker run $EXTRA_ARGS myimage
# 仅对本行禁用 SC2010
find . -name '*.tmp' # shellcheck disable=SC2010
九、落地清单
| 步骤 | 动作 |
|---|---|
| 1 安装 | 本地 brew/apt;CI 用 action-shellcheck |
| 2 基线 | 存量脚本先全量扫一遍,按 error > warning 修 |
| 3 卡口 | PR 必过 ShellCheck,否则禁止合并 |
| 4 钩子 | pre-commit 自动扫新增脚本 |
| 5 规范 | 团队约定「变量必引号、用数组代替字串拆分」 |
十、常见 SC 规则编号速查
记住几个高频 SC 编号,看报告能直接定位问题,也方便在代码评审里精准引用,而不是笼统地说「这段写得不对」:
| 编号 | 含义 | 典型场景 |
|---|---|---|
| SC2086 | 变量未加双引号 | for 循环、命令参数传递 |
| SC2046 | 命令替换未引号 | $(…) 参与分词 |
| SC2181 | 误用 $? 判断 | set -e 下的分支处理 |
| SC2034 | 变量已定义未使用 | 拼写错误、死代码 |
| SC2010 | find 与 ls 混用 | 应改用 find -name 替代 |
| SC2317 | 函数内 exit 不可达 | if 分支里的逻辑错误 |
十一、配合 shfmt 统一格式:分析 + 格式化双保险
ShellCheck 管「对不对」,shfmt 管「丑不丑」。两者一起用,既消灭隐患又统一团队风格,评审时也不会再为缩进吵架:
# 格式化并原地改写:4 空格缩进、对齐 case 分支
shfmt -i 4 -ci -w deploy.sh
# pre-commit 里同时跑 shfmt + shellcheck
repos:
- repo: https://github.com/mvdan/sh
rev: v3.11.0
hooks:
- id: shfmt
args: ["-i", "4", "-ci", "-w"]
十二、把 ShellCheck 写进团队规范
工具要真正生效,得写进 CONTRIBUTING.md:规定「所有 .sh 必须过 ShellCheck 且无 error」「新增脚本提交前先跑 pre-commit」。再配一段 CI 卡口,新人第一次提 PR 就被自动教会,比任何口头约定都可靠。当 ShellCheck 成为仓库的默认门槛,Bash 事故率会肉眼可见地往下掉。
十三、在 Docker 构建里顺手扫一遍
脚本常常随镜像一起发布。除了 CI,也可以在多阶段构建里用 ShellCheck 官方镜像扫描,保证进到生产镜像的脚本同样合规(参考《Docker 实战》的多阶段构建思路):
# 第一阶段:扫描脚本,有 error 直接构建失败
FROM koalaman/shellcheck:stable AS lint
COPY scripts/ /scripts/
RUN find /scripts -name '*.sh' | xargs shellcheck -S error
# 第二阶段:只有通过的脚本才进最终镜像
FROM alpine:3.20
COPY --from=lint /scripts/ /usr/local/bin/
最危险的场景往往是一个没引号的变量撞上通配符。比如 rm -rf /data/$user/*,当 $user 为空时会被展开成 rm -rf /data/* 直接删光整盘。ShellCheck 会标 SC2115 提醒变量未引号导致路径越界,正确的写法必须带引号并先判空:
user="$1"
# SC2115:变量带引号并判空,避免空值越界删除
if [ -z "$user" ]; then
echo "usage: $0 <user>"
exit 1
fi
rm -rf "/data/$user"/*
十四、常见问题速答
| 问题 | 答案 |
|---|---|
| ShellCheck 能自动修脚本吗? | 只报不修,可配合 shfmt 做格式化改写 |
| 遇到误报怎么处理? | 用 shellcheck disable 精准忽略单条规则,别全局关 |
| 放进 CI 会拖慢构建吗? | 纯静态扫描、秒级完成,放心进流水线 |
| 只写一行脚本也要扫吗? | 建议扫,越是短脚本越容易漏掉引号 |
ShellCheck 不替你写业务逻辑,但能挡掉八成低级事故。把它当成 Bash 的「编译器警告」,养成「写完先 shellcheck 一遍」的肌肉记忆,比事后救火省心得多。




