この記事の狙い
Bash スクリプトの品質を自動で底上げするために、shellcheck(静的解析)と shfmt(整形)の運用を最小コストで組み込みます。
ローカル実行・CI・差分運用・一時的な除外の扱いまで、コピペで導入できる形でまとめます。
前提と対象
Bash 4 以上を想定します。Linux と macOS での導入を中心に扱い、Windows(WSL)でも同様に動きます。
目的は習慣化です。複雑なルール作りの前に、まずは「常に走る最小セット」を確立します。
TL;DR(最小実装・コピペ可)
次の 3 コマンドを手に覚えさせます。ローカルでも CI でも同じ顔をしているのが理想です。
# 1) 静的解析(可能なら全ファイル)
shellcheck scripts/*.sh src/**/*.sh
# 2) 整形のチェック(修正はしない/差分のみ表示)
shfmt -d .
# 3) 自動整形(ローカルのときだけ)
shfmt -w .
インストールの最短ルートは次のいずれかです。
# macOS/Homebrew
brew install shellcheck shfmt
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y shellcheck shfmt
# Arch
sudo pacman -S shellcheck shfmt
設計の要点(原則)
静的解析と整形は別物です。shfmt はコードレイアウトを統一し、shellcheck は言語仕様と落とし穴を検査します。
まずは整形を常時・解析は警告のノイズを減らしつつ継続、という順序で導入すると摩擦が小さくなります。
ステップ実装(分解解説)
段階1:整形の“基準”を決めて固定する
shfmt はデフォルトでも十分実用的ですが、チームで迷いにくい最小オプションだけ足しておきます。
# プロジェクト標準(例)
shfmt -i 2 -ci -sr -kp -d .
本文で使うスイッチの意味は短く把握しておきます。-i 2 はインデント幅 2、-ci は case にもインデントを効かせる、-sr は簡略な if then 改行などのリライタ、-kp は printf -- などのコマンド位置保持です。
ローカル保存時に自動整形するなら -w を使い、CI では -d にして差分があれば失敗にします。
段階2:静的解析の“閾値”を合わせる
shellcheck は初回導入で警告が多く出やすいので、段階的に厳しくします。
まずは致命的なもの(未定義変数、未クォート、[ と [[ の混同など)を潰し、残りは一時的に抑制します。
# 致命的だけ通す例(SC2002のようなスタイル警告は後回し)
shellcheck -S warning scripts/*.sh
抑制はピンポイントで行います。ファイル全体やディレクトリ単位の無効化は最後の手段にします。
# 直前1行だけ抑制(例:意図的に単語分割したい場面)
# shellcheck disable=SC2086
cp $src_dir/*.txt "$dest/"
抑制コメントの直下には意図を残します。レビュー時に「妥当かどうか」を即判断できます。
段階3:ローカルの“ワンライナー”を体に染み込ませる
迷ったら次で走らせます。対象が多いときは git ls-files 連携が便利です。
# Git 管理下の .sh とシェバン付きを対象に
git ls-files -z | xargs -0 -- shellcheck
git ls-files -z | xargs -0 -- shfmt -d
対象拡張子が混在している場合は、シェバン検出で拾うワンライナーが堅実です。
# シェバンでbash/shを検出して整形(GNU xargs 想定)
grep -rlZ '^#!.*/\(ba\)\?sh' -- . | xargs -0 shfmt -d
段階4:CI に“落ち方”を教える
CI では直してくれない代わりに落ち方が明確であることが重要です。差分をログに出し、リンクを貼ります。
# .github/workflows/shellcheck-shfmt.yml
name: lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt-get update && sudo apt-get install -y shellcheck shfmt
- name: shellcheck
run: shellcheck -S warning $(git ls-files '*.sh')
- name: shfmt (diff)
run: |
set -e
shfmt -i 2 -ci -sr -kp -d .
落ちたときの修正ガイドは README に 2 行で書いておきます。
「shfmt -w . を実行して再コミット」「shellcheck の指摘はコメントに従って修正 or # shellcheck disable=SCxxxx を添える」です。
段階5:エディタ連携で“無意識化”
保存時整形を使うと運用コストが急落します。.editorconfig を併用してインデントぶれを抑えます。
# .editorconfig
root = true
[*.{sh,bash}]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
shfmt プラグインや LSP(bash-language-server)と組み合わせると、診断と整形が書いている最中に走ります。
失敗しやすい点(アンチパターン)
静的解析の“誤検知”に苛立って全無効化してしまうのは典型的な失敗です。
まずは危険系の警告(未クォート、未定義、配列の誤展開、条件式の演算子混同)を残し、スタイル系は順次対応に回します。shfmt -w を CI で実行して機械的にコミットさせるのも避けます。人間のレビューと競合しやすく、履歴が汚れます。
テストと品質(最小の合わせ技)
整形と解析はテストの前座として常に走らせます。
次の順番が摩擦が少なく、失敗時の原因切り分けが容易です。
shfmt -d(書式の差分があればここで落ちる)shellcheck(言語的な問題を止める)- 最小テスト(
batsや生 Bash の比較テスト)
運用設計(実務)
モノレポや古い資産を扱う現場では、ディレクトリ単位の段階導入が効きます。
新規ディレクトリは初回コミットで shfmt -w を通し、shellcheck は -S warning から始めて新規コードにだけ厳格を求めます。
既存コードは触れた周辺だけ直す方針にすると、移行コストを抑えられます。
互換性と移植性
shfmt は bash/sh/mksh/zsh をサポートしますが、言語方言によっては再整形が期待通りでない場合があります。bash 拡張を多用する場合は、シェバンを正しく書くこと、shfmt に** -ln=bash** を渡すことを検討します。
# bash 方言を明示
shfmt -ln=bash -i 2 -ci -sr -kp -d .
セキュリティと安全設計
shellcheck の中でも SC2086(未クォートの単語分割)、SC2046(コマンド置換の未クォート)、SC2164(cd の失敗未検知) などは実害直結です。
抑制する前に本当に必要かを見直し、必要なときだけ直上 1 行に限定して無効化します。
パフォーマンスの勘所(短く)
リポジトリが大きい場合は差分のみを対象にします。
CI では git diff --name-only $BASE... | xargs shellcheck のようにして、レビュー対象のファイルだけを検査します。
# 直近の差分のみ検査(PRベース)
changed="$(git diff --name-only origin/main... | grep -E '\.sh$' || true)"
[ -z "$changed" ] || shellcheck $changed
[ -z "$changed" ] || shfmt -d $(printf '%s\n' "$changed" | xargs)
参考実装(拡張版・コピペ可)
プロジェクトに入れる最低限のスクリプトを 1 本だけ置いておくと、初学者も迷いません。
# tools/lint.sh
#!/usr/bin/env bash
set -Eeuo pipefail
files="$(git ls-files | grep -E '\.sh$' || true)"
if [ -z "$files" ]; then
echo "no shell scripts"; exit 0
fi
echo "[shfmt] formatting check..."
shfmt -i 2 -ci -sr -kp -d .
echo "[shellcheck] static analysis..."
shellcheck -S warning $files
echo "OK"
README の導入手順には、この 2 行を添えるだけで十分です。
bash tools/lint.sh # CI と同じ検査
shfmt -w . # ローカルで自動整形(コミット前)
よくある質問(Q&A)
Q. 警告が多すぎて前に進めません。
A. 重大系だけを残して段階導入してください。-S warning から入り、潰せたら -S style に段を上げます。
Q. どうしても抑制が必要です。
A. 直上 1 行に限定し、理由を一言添えます。ファイル全体の抑制は最終手段にします。
Q. エディタ整形と shfmt の結果が微妙に違います。
A. .editorconfig と shfmt のオプションを合わせます。可能なら保存時に shfmt を直接呼ぶ設定に寄せます。
