Bash 代码检查(Linting)
集成 Shellcheck 和 shfmt,用于保证 bash 脚本质量。
Shellcheck
安装
# Debian/Ubuntu
apt install shellcheck
# macOS
brew install shellcheck
# 从源码安装
cabal update && cabal install ShellCheck
基本用法
# 检查单个文件
shellcheck script.sh
# 检查多个文件
shellcheck *.sh
# 指定 shell 方言
shellcheck --shell=bash script.sh
shellcheck --shell=sh script.sh
# 排除特定规则
shellcheck --exclude=SC2086 script.sh
shellcheck --exclude=SC2086,SC2046 script.sh
# 输出格式
shellcheck --format=gcc script.sh # GCC 风格
shellcheck --format=json script.sh # JSON 格式(适合工具链)
shellcheck --format=diff script.sh # 统一差异格式
常见 Shellcheck 错误码
| 代码 | 问题 | 修复 |
|---|---|---|
| SC2086 | 缺少双引号,可能导致通配符/分词错误 | "$var" |
| SC2046 | 命令替换需要使用引号 | "$(cmd)" |
| SC2006 | 请使用 $() 代替反引号 | $(cmd) |
| SC2034 | 变量似乎未使用 | 删除或导出 |
| SC2155 | 应分开声明和赋值 | 拆分为 local var; var=$(...) |
| SC2164 | 使用 `cd ... | |
| SC2181 | 直接检查命令退出状态 | if cmd; then |
| SC2129 | 考虑将写入操作分组 | 使用 { } > file |
| SC1090 | 无法跟踪被 source 的文件 | 使用 # shellcheck source=路径 |
| SC2154 | 引用了未赋值的变量 | 初始化或声明 |
Shellcheck 指令
# 禁用下一行检查
# shellcheck disable=SC2086
echo $unquoted_var
# 禁用整个文件的检查(放在文件顶部)
# shellcheck disable=SC2086,SC2046
# 为 source 命令指定源文件
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"
# 指定 shell 方言
# shellcheck shell=bash
# 禁用块的检查(不支持 - 请使用逐行禁用)
行内指令模式
# 禁用特定警告并附上说明
# shellcheck disable=SC2034 # 该变量由 source 脚本使用
readonly CONFIG_VERSION="1.0"
# 禁用多个错误码
# shellcheck disable=SC2086,SC2046
result=$(echo $var)
# 动态路径的 source 指令
# shellcheck source=/dev/null
source "${DYNAMIC_PATH}/config.sh"
shfmt
安装
# macOS
brew install shfmt
# Go 安装
go install mvdan.cc/sh/v3/cmd/shfmt@latest
# Snap
snap install shfmt
# 二进制下载
# 来自 https://github.com/mvdan/sh/releases
基本用法
# 格式化并输出到标准输出
shfmt script.sh
# 原地格式化文件
shfmt -w script.sh
# 检查格式(如果未格式化则退出码为 1)
shfmt -d script.sh
# 递归处理目录
shfmt -w .
shfmt -w scripts/
格式化选项
# 缩进
shfmt -i 2 script.sh # 2 空格缩进
shfmt -i 4 script.sh # 4 空格缩进
shfmt -i 0 script.sh # 制表符(默认)
# 二元运算符放在行首
shfmt -bn script.sh
# switch 分支缩进
shfmt -ci script.sh
# 重定向运算符后跟空格
shfmt -sr script.sh
# 保留列对齐填充
shfmt -kp script.sh
# 函数左大括号另起一行
shfmt -fn script.sh
# 组合使用
shfmt -i 4 -ci -bn script.sh
配置 (.editorconfig)
# .editorconfig
[*.sh]
indent_style = space
indent_size = 4
shell_variant = bash
binary_next_line = true
switch_case_indent = true
space_redirects = true
示例转换
使用 shfmt 前:
if [ -f "$file" ];then
echo "exists"
fi
for i in 1 2 3;do
process $i
done
使用 shfmt -i 4 -ci 后:
if [ -f "$file" ]; then
echo "exists"
fi
for i in 1 2 3; do
process $i
done
Pre-commit 集成
.pre-commit-config.yaml
repos:
- repo: https://github.com/koalaman/shellcheck-precommit
rev: v0.9.0
hooks:
- id: shellcheck
args: ["--severity=warning"]
- repo: https://github.com/scop/pre-commit-shfmt
rev: v3.7.0-1
hooks:
- id: shfmt
args: ["-i", "4", "-ci", "-w"]
# 替代方案:本地钩子
- repo: local
hooks:
- id: shellcheck
name: shellcheck
entry: shellcheck
language: system
types: [shell]
args: ["--severity=warning", "-x"]
- id: shfmt
name: shfmt
entry: shfmt
language: system
types: [shell]
args: ["-i", "4", "-ci", "-w"]
运行 Pre-commit
# 安装钩子
pre-commit install # 或:prek install
# 在所有文件上运行
pre-commit run --all-files # 或:prek run --all-files
# 运行特定钩子
pre-commit run shellcheck --all-files
pre-commit run shfmt --all-files
# 在特定文件上运行
pre-commit run --files script.sh
集成到 CI/CD
GitHub Actions
name: Shell Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 运行 ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: warning
- name: 使用 shfmt 检查格式
uses: mvdan/github-action-shfmt@master
with:
flags: -d -i 4 -ci
GitLab CI
shellcheck:
image: koalaman/shellcheck-alpine:stable
script:
- find . -name "*.sh" -exec shellcheck {} +
shfmt:
image: mvdan/shfmt:latest
script:
- shfmt -d -i 4 -ci .
修复常见问题
SC2086:使用引号防止分词
# 错误
echo $var
# 正确
echo "$var"
printf '%s\n' "$var"
SC2155:分开声明和赋值
# 错误 - 隐藏了退出状态
local var=$(some_command)
# 正确
local var
var=$(some_command)
SC2164:使用 cd || exit
# 错误
cd "$dir"
rm -rf *
# 正确
cd "$dir" || exit 1
rm -rf *
# 或者使用子 shell
(cd "$dir" && rm -rf *)
SC2181:直接检查退出状态
# 错误
command
if [ $? -eq 0 ]; then
# 正确
if command; then
SC1090/SC1091:Source 问题
# 为动态 source 添加指令
# shellcheck source=/dev/null
source "$DYNAMIC_PATH/lib.sh"
# 或者指定实际路径
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"
编辑器集成
VS Code
安装 Timon Wong 开发的 "ShellCheck" 扩展。
// settings.json
{
"shellcheck.enable": true,
"shellcheck.run": "onSave",
"shellcheck.executablePath": "shellcheck",
"editor.formatOnSave": true,
"[shellscript]": {
"editor.defaultFormatter": "foxundermoon.shell-format"
}
}
Vim/Neovim
" 使用 ALE
let g:ale_linters = {'sh': ['shellcheck']}
let g:ale_fixers = {'sh': ['shfmt']}
let g:ale_sh_shfmt_options = '-i 4 -ci'
" 使用 coc.nvim
" 安装 coc-sh 扩展
最佳实践
- 尽早运行 shellcheck - 集成到编辑器和 CI 中
- 修复问题,不要抑制警告 - 仅在充分理由下才禁用
- 记录抑制原因 - 解释为什么禁用规则
- 使用严重级别 - CI 中使用
--severity=warning - 保持一致的格式 - 在 pre-commit 中使用 shfmt
- 锁定工具版本 - 在 CI/pre-commit 中固定版本