Bash 腳本 Lint
整合 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 # 統一 diff
常用 ShellCheck 規則碼
| 規則碼 | 問題描述 | 修正方式 |
|---|---|---|
| SC2086 | 雙引號,避免因 glob/splitting 導致問題 | "$var" |
| SC2046 | 命令替換請加上引號 | "$(cmd)" |
| SC2006 | 使用 $() 代替反引號 | $(cmd) |
| SC2034 | 變數似乎未使用 | 移除或匯出 |
| SC2155 | 應分開宣告與賦值 | 拆成 local var; var=$(...) |
| SC2164 | 使用 cd ... || exit | 處理 cd 失敗 |
| SC2181 | 直接檢查指令的退出狀態 | if cmd; then |
| SC2129 | 考慮將多個寫入動作合併 | 使用 { } > file |
| SC1090 | 無法追蹤來源檔 | 使用 # 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
基本用法
# 格式化並輸出到 stdout
shfmt script.sh
# 原地格式化
shfmt -w script.sh
# 檢查格式(若未格式化則 exit 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 case 縮排
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"]
# 替代方案:本機 hooks
- 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
# 安裝 hooks
pre-commit install # 或使用:prek install
# 對所有檔案執行
pre-commit run --all-files # 或使用:prek run --all-files
# 執行特定 hook
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:加上引號避免 splitting
# 錯誤
echo $var
# 正確
echo "$var"
printf '%s\n' "$var"
SC2155:分開宣告與賦值
# 錯誤 ─ 會遮蓋 exit status
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:來源檔案問題
# 對動態來源加上指令
# 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 中鎖定版本