ShellCheck 能检测哪些 shell 脚本问题?

更新时间

ShellCheck 是一个开源的 shell 脚本静态分析工具,它能在不运行脚本的情况下自动找出 bug。它主要检测三类问题:语法与引用错误、可移植性问题,以及较高层的语义问题。适用条件是:你有一段 sh/bash 脚本,希望在上线或执行前发现潜在错误;你可以把脚本粘贴到 shellcheck.net 在线试用,也可以用 cabal、apt、dnf、pkg 或 brew 在本地安装。

语法与引用问题

这是 ShellCheck 最基础也最常见的一类检测,典型场景是引号使用不当。

官网示例中有一段脚本,用单引号包裹了包含变量名的提示信息:

echo 'Backup not enabled in $HOME/.myconfig, exiting'

单引号会让 $HOME 不被展开,用户看到的是一条字面文本而不是实际路径。ShellCheck 会指出这类引用问题。同一示例里还有 find 命令中未加引号的 *.tar.gz 通配模式,以及变量 $verbose 直接拼进命令行的写法,都属于引用与展开相关的隐患。

这类问题的共同点是:脚本能跑,但结果和作者意图不一致,或者在某些文件名、参数下突然出错。

可移植性问题

ShellCheck 会检查 shebang 声明与脚本实际写法是否匹配。官网给出的例子是脚本开头写 #!/bin/sh,但正文使用了 bash 专有语法:

#!/bin/sh
for n in {1..$RANDOM}
do
  ...
  if (( n % 3 == 0 ))
  ...
done

{1..$RANDOM} 的花括号展开和 (( )) 算术语法都不是 POSIX sh 的标准能力。ShellCheck 会提示这种可移植性冲突,并建议把 shebang 改为 #!/bin/bash 以允许 bashism。如果你的脚本要在多种 shell 或精简环境(如某些容器基础镜像)中运行,这类提示尤其值得处理。

较高层的语义问题

除了逐行的语法检查,ShellCheck 还能发现一些跨语句的逻辑问题。官网示例包括:

  • getopts 分支不完整:while getopts "nf:" param 的 case 中只处理了 f) 和 v),n 选项没有对应分支,脚本行为可能与预期不符。
  • 文件类型处理分支:case "$file" 中同时出现 *.gz) 和 *.tar.gz),前者会先匹配到 .tar.gz 文件,导致后者永远不生效。
  • 命令替换写法错误:if [[ "$$(uname)" == "Linux" ]] 中 $$ 是当前进程 PID,作者本意应是 $(uname),这类笔误很难靠肉眼发现。

这些问题的价值在于:它们不是语法错误,脚本照样能执行,但逻辑已经偏离意图。

常见错误类型速查

问题类型 示例中的表现 后果
引号使用不当 单引号内写 $HOME 变量不展开,提示信息错误
未引用变量/通配符 $verbose、*.tar.gz 直接拼接 参数含空格或特殊字符时命令出错
shebang 与语法冲突 #!/bin/sh 配 (( )) 换 shell 环境后脚本失败
选项分支缺失 getopts 中漏掉 n) 选项被静默忽略
分支顺序错误 *.gz) 排在 *.tar.gz) 前 后续分支不可达
命令替换笔误 $$(uname) 判断条件永远不成立

怎么用起来

在线方式:打开 shellcheck.net,把脚本粘贴进编辑器,输出窗口会显示检测结果,页面还提供 “Apply fixes” 应用修复和 “Report bug” 反馈问题。本地方式:按官网说明用 apt、dnf、pkg、brew 或 cabal 安装后,在命令行对脚本文件运行检查。它也被集成到主流编辑器中作为 linter,并可在 CodeClimate、Codacy、CodeFactor 上自动检查 GitHub 仓库。

使用时的一个实用建议:先处理引用和可移植性两类提示,它们最容易引发实际故障;语义类提示则需要结合脚本意图判断,因为工具给出的是可疑点而非确定错误。