Bash リンティング
Shellcheck と shfmt を統合した Bash スクリプトの品質保証。
Shellcheck
インストール
# Debian/Ubuntu
apt install shellcheck
# macOS
brew install shellcheck
# ソースから
cabal update && cabal install ShellCheck
基本的な使い方
# 単一ファイルをチェック
shellcheck script.sh
# 複数ファイルをチェック
shellcheck *.sh
# 特定のシェル方言を指定
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 | グロブ/分割を防ぐためにダブルクォートする | "$var" |
| SC2046 | コマンド置換をクォートする | "$(cmd)" |
| SC2006 | バッククォートではなく $() を使用する | $(cmd) |
| SC2034 | 変数が未使用 | 削除またはエクスポート |
| SC2155 | 宣言と代入を別々に行う | local var; var=$(...) と分割 |
| SC2164 | cd ... || exit を使用する | cd の失敗を処理 |
| SC2181 | 終了ステータスを直接チェックする | if cmd; then |
| SC2129 | 書き込みのグループ化を検討する | { } > file を使用 |
| SC1090 | ソースされたファイルを追跡できない | # shellcheck source=path を使用 |
| SC2154 | 変数が参照されているが代入されていない | 初期化または宣言 |
Shellcheck ディレクティブ
# 次の行を無効化
# shellcheck disable=SC2086
echo $unquoted_var
# ファイル全体を無効化(先頭に)
# shellcheck disable=SC2086,SC2046
# ソース元ファイルを指定
# shellcheck source=./lib/functions.sh
source "$SCRIPT_DIR/lib/functions.sh"
# シェル方言を指定
# shellcheck shell=bash
# ブロックを無効化(未サポート - 行単位で使用)
インラインディレクティブパターン
# 説明付きで特定の警告を無効化
# shellcheck disable=SC2034 # Variable used by sourcing script
readonly CONFIG_VERSION="1.0"
# 複数コードを無効化
# shellcheck disable=SC2086,SC2046
result=$(echo $var)
# 動的パスのソースディレクティブ
# 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 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"]
# 代替: ローカルフック
- 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: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
severity: warning
- name: Check formatting with 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 *
# またはサブシェルを使用
(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 でバージョンを固定する