ShellCheck 实战:用静态分析把 Bash 脚本 Bug 挡在运行前

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变量已定义未使用拼写错误、死代码
SC2010find 与 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 一遍」的肌肉记忆,比事后救火省心得多。

上一篇 数据库压测实战:用 sysbench 与 pgbench 量化性能
下一篇 SSH 连接故障排查实战:超时、拒绝与 host key 变更
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]