Bashスクリプトデバッグの習得:開発者向け必須テクニック

構文チェック、xtrace、ストリクトモード、トラップ、ShellCheck、フォーカスしたログでBashスクリプトをデバッグします。

Bashスクリプトデバッグの習得:開発者向け必須テクニック

Bashスクリプトのデバッグは、スクリプトが期待と異なる動作をした箇所を特定することから始まります。優れたデバッグ手法により、構文エラー、変数の展開、コマンドの順序、終了ステータスを、スクリプトをノイズで埋め尽くすことなく可視化できます。

このガイドでは、実際の自動化スクリプトがcron、CI、本番環境に到達する前に使用できる実践的なBashデバッグテクニックを紹介します。

構文チェックから始める

ランタイムの動作をトレースする前に、Bashがファイルを解析できることを確認します:

bash -n ./deploy.sh

bash -nはスクリプトを読み込み、コマンドを実行せずに構文エラーを報告します。fidonethen、引用符、括弧の欠落を検出します。ただし、ロジックエラー、ファイルの欠落、実行時に失敗するコマンドは検出しません。

例えば、以下のタイプミスは何も実行される前に検出されます:

if [ -f "$CONFIG" ]; then
    echo "Config found"
# fiが欠落

大規模な編集後や、さらにデバッグ出力を追加する前に構文チェックを実行しましょう。

set -xで実行をトレースする

最も便利な組み込みデバッガはxtraceです:

set -x
some_command "$VALUE"
set +x

トレースを有効にすると、Bashは各コマンドを展開後、実行前に出力します。これにより、変数が空かどうか、グロブが展開されたかどうか、コマンドが想定と異なる引数を受け取ったかどうかを確認できます。

スクリプト全体をトレースするには、以下を実行します:

bash -x ./deploy.sh

よりクリーンなトレースのために、PS4を設定して各行にソースの行番号を含めます:

export PS4='+ ${BASH_SOURCE}:${LINENO}: '
bash -x ./deploy.sh

スクリプトが機密情報を扱う場合、トークン、パスワード、署名付きURLを出力するセクションはトレースしないでください。それらのコマンドの前にトレースをオフにします:

set +x
login_with_secret "$API_TOKEN"
set -x

ストリクトモードを慎重に追加する

以下のオプションは、一般的な失敗を早期にキャッチします:

set -euo pipefail

set -eは、多くの未処理のコマンド失敗時に終了します。set -uは、未設定の変数をエラーとして扱います。set -o pipefailは、パイプライン内のいずれかのコマンドが失敗した場合(最後のコマンドだけでなく)にパイプラインを失敗させます。

これらは便利ですが、明示的な処理の代わりにはなりません。grepのようなコマンドは、通常の「見つからない」結果に対して1を返すことがあります:

if grep -q "READY" status.txt; then
    echo "ready"
else
    echo "not ready"
fi

これは、grep -q "READY" status.txt || trueで結果を隠すよりも明確です。

適切な値を出力する

フォーカスしたログは、散在するecho行よりも優れています。デバッグしている分岐に影響を与える値を出力します:

printf 'DEBUG: user=%q env=%q target=%q\n' "$USER_NAME" "$ENVIRONMENT" "$TARGET_HOST" >&2

printf '%q'はシェルエスケープされた値を表示するため、スペースや特殊文字を簡単に特定できます。デバッグ出力はstderrに送信し、通常のスクリプト出力がパイプラインで使用可能なままにします。

コマンドが失敗した場合、そのステータスをすぐにキャプチャします:

run_migration
status=$?

if [ "$status" -ne 0 ]; then
    echo "Migration failed with exit code $status" >&2
    exit "$status"
fi

$?を保存する前に別のコマンドを実行しないでください。echoでも置き換えられます。

ループと条件文のデバッグ

ループのバグは、多くの場合、単語分割や予期しない入力から発生します。変数を引用符で囲み、行を安全に読み取ります:

while IFS= read -r line; do
    printf 'line=%q\n' "$line" >&2
done < input.txt

条件文の場合、比較される正確な値を出力します:

printf 'expected=%q actual=%q\n' "$EXPECTED" "$ACTUAL" >&2

if [[ "$ACTUAL" == "$EXPECTED" ]]; then
    echo "match"
fi

ローカルデバッグ中にスクリプト内で一時停止する必要がある場合、readが機能します:

read -r -p "Press Enter to continue..."

スクリプトをコミットする前に、特に無人実行される可能性がある場合は、一時停止を削除します。

静的解析にShellCheckを使用する

ShellCheckは、Bashが喜んで実行するが、コーナーケースで壊れる可能性のある多くの問題をキャッチします:

shellcheck ./deploy.sh

引用符で囲まれていない変数、到達不能コード、疑わしいテスト、未使用の変数、移植性の問題をフラグします。警告は、コードを検査するためのプロンプトとして扱い、スクリプトが間違っているという自動的な証明として扱わないでください。意図的に警告を無効にすることもありますが、その理由を短いコメントで追加してください。

trapを使用して失敗行を特定する

長いスクリプトの場合、エラートラップが失敗が発生した場所を教えてくれます:

set -Eeo pipefail

trap 'echo "Error on line $LINENO: $BASH_COMMAND" >&2' ERR

set -Eは、Bashの関数やサブシェルにERRトラップを伝播させるのに役立ちます。これは、インタラクティブシェルがない可能性があるCIログで便利です。

まとめ

bash -nから始め、ランタイムトレースにはbash -xまたは対象を絞ったset -xを使用し、誤った動作をする分岐の周りにフォーカスしたstderrログを追加します。重要なスクリプトには、ShellCheckを実行し、ERRトラップを追加して、失敗が注意が必要なコマンドと行を指し示すようにします。