shellcheck/shfmt 運用|静的解析と整形

テスト&品質

この記事の狙い

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、-cicase にもインデントを効かせる、-sr は簡略な if then 改行などのリライタ、-kpprintf -- などのコマンド位置保持です。
ローカル保存時に自動整形するなら -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 で実行して機械的にコミットさせるのも避けます。人間のレビューと競合しやすく、履歴が汚れます。

テストと品質(最小の合わせ技)

整形と解析はテストの前座として常に走らせます。
次の順番が摩擦が少なく、失敗時の原因切り分けが容易です。

  1. shfmt -d(書式の差分があればここで落ちる)
  2. shellcheck(言語的な問題を止める)
  3. 最小テスト(bats や生 Bash の比較テスト)

運用設計(実務)

モノレポや古い資産を扱う現場では、ディレクトリ単位の段階導入が効きます。
新規ディレクトリは初回コミットで shfmt -w を通し、shellcheck-S warning から始めて新規コードにだけ厳格を求めます。
既存コードは触れた周辺だけ直す方針にすると、移行コストを抑えられます。

互換性と移植性

shfmtbash/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. .editorconfigshfmt のオプションを合わせます。可能なら保存時に shfmt を直接呼ぶ設定に寄せます。

参考リンク

Bash玄

はじめまして!Bash玄です。

エンジニアとしてシステム運用に携わる中で、手作業の多さに限界を感じ、Bashスクリプトを活用して業務を効率化したのがきっかけで、この道に入りました。「手作業は負け」「スクリプトはシンプルに」をモットーに、誰でも実践できるBashスクリプトの書き方を発信しています。

このサイトでは、Bashの基礎から実践的なスクリプト作成まで、初心者でもわかりやすく解説しています。少しでも「Bashって便利だな」と思ってもらえたら嬉しいです!

# 好きなこと
- シンプルなコードを書くこと
- コマンドラインを快適にカスタマイズすること
- 自動化で時間を生み出すこと

# このサイトを読んでほしい人
- Bashに興味があるけど、何から始めればいいかわからない人
- 定型業務を自動化したい人
- 効率よくターミナルを使いこなしたい人

Bashの世界に一歩踏み出して、一緒に「Bash道」を極めていきましょう!

Bash玄をフォローする