高度なBashスクリプティング:エラーハンドリングのベストプラクティス

厳格モード、明示的なチェック、クリーンアップトラップ、明確な終了コード、stderrログを使用してBashのエラーハンドリングを改善します。

高度なBashスクリプティング:エラーハンドリングのベストプラクティス

Bashスクリプトのエラーハンドリングは、小さな自動化のミスが深刻な本番環境の問題に発展するのを防ぎます。バックアップの失敗、APIコールのエラー、一時ファイルの残置などが発生した場合、スクリプトは明確に停止し、システムを既知の状態に保つべきです。

これらのパターンは、スクリプトがファイルを変更したり、コードをデプロイしたり、リモートサービスと通信したり、端末を監視せずに実行される場合に使用します。

基礎:終了コードの理解

Bashで実行されるすべてのコマンドは、成功または失敗に関わらず、終了ステータス(終了コード)を返します。これはコマンドの結果を通知する基本的なメカニズムです。

  • 終了コード0: 成功した実行を示します。慣例により、0は成功を意味します。
  • 終了コード1-255(非ゼロ): エラー、失敗、または警告を示します。特定の非ゼロコードは、特定のエラータイプを示すことがよくあります(例:1は一般的なエラー、2はシェルコマンドの誤用)。

最新の終了ステータスは、特別な変数$?に格納されます。

# 成功したコマンド
ls /tmp
echo "Status: $?"
# Status: 0

# 失敗したコマンド(存在しないファイル)
cat /nonexistent_file
echo "Status: $?"
# Status: 1(またはそれ以上、エラーによる)

必須のベストプラクティス:厳格モードの実装

本格的なBashスクリプトでは、シバン行の直後に3つのディレクティブを配置する必要があります。これらはまとめて「厳格モード」と呼ばれることがよくあります。これにより、スクリプトは前提条件が満たされない場合に早期に失敗するようになります。

1. エラー時に即座に終了する(set -e

set -eまたはset -o errexitコマンドは、コマンドが非ゼロのステータスで終了した場合に、Bashに即座にスクリプトを終了するよう指示します。これにより、連鎖的な障害を防ぎます。

警告: set -eは、条件テスト(ifwhile)内、またはコマンドが&&||リストの一部である場合には無視されます。失敗ステータスは、周囲の構造によって明示的に使用される必要があります。

2. 未設定変数をエラーとして扱う(set -u

set -uまたはset -o nounsetコマンドは、設定されていない変数(例:$FILENAMEの代わりに$FIELNAMEと誤って入力する)を使用しようとすると、スクリプトが即座に終了するようにします。これにより、空の変数や意図しない変数に起因するデバッグが困難なバグを防ぎます。

3. パイプラインでのエラー処理(set -o pipefail

デフォルトでは、一連のコマンドがパイプで連結されている場合(例:cmd1 | cmd2 | cmd3)、Bashは最後のコマンド(cmd3)の終了ステータスのみを報告します。cmd1が失敗しても、スクリプトは正常に実行を続ける可能性があります。

set -o pipefailは、パイプラインの終了ステータスが、失敗した最後のコマンドの終了ステータスになるか、すべてのコマンドが成功した場合はゼロになることを保証します。これは信頼性の高いデータ処理に不可欠です。

標準の厳格モードヘッダー

高度なスクリプトは常に、この堅牢なヘッダーから始めてください:

#!/bin/bash

set -euo pipefail

一部の古いテンプレートでは、IFS=$'\n\t'も設定します。これがスクリプトの残りの部分での単語分割にどのように影響するかを理解している場合にのみ使用してください。変数を引用符で囲み、while IFS= read -r lineで入力を読み取る方が通常は明確です。

条件付きエラーチェック

set -eは予期しないエラーを処理しますが、特定の条件をチェックしたり、カスタムエラーメッセージを提供したりする必要があることがよくあります。

if文とカスタム関数の使用

set -eのみに依存する代わりに、ifブロックを使用して既知の潜在的な障害を適切に処理し、説明的な出力を提供します。

# 一貫性のためにカスタムエラー関数を定義
error_exit() {
    printf '[FATAL] %s\n' "$1" >&2
    exit 1
}

TEMP_DIR="/tmp/data_processing_$(date +%s)"

# ディレクトリ作成が成功したかどうかを確認
if ! mkdir -p "$TEMP_DIR"; then
    error_exit "Failed to create temporary directory: $TEMP_DIR"
fi

echo "Temporary directory created successfully: $TEMP_DIR"

# 処理前にファイルが存在するかどうかを確認する例
FILE_TO_PROCESS="input.csv"

if [[ ! -f "$FILE_TO_PROCESS" ]]; then
    error_exit "Input file not found: $FILE_TO_PROCESS"
fi

ショートサーキットロジック(&&および||

単純な順次操作には、ショートサーキット演算子を使用します。これは非常に読みやすく簡潔です。

  • 成功チェーン(&&): 最初のコマンドが成功した場合にのみ、2番目のコマンドが実行されます。
  • 失敗キャッチ(||): 最初のコマンドが失敗した場合にのみ、2番目のコマンドが実行されます。
# セットアップを実行し、成功した場合に処理を実行、セットアップが失敗した場合は失敗
setup_environment && process_data

# 接続を試み、失敗した場合はメッセージを表示して正常に終了
ssh user@server || { echo "Connection failed, check network settings." >&2; exit 2; }

trapによるグレースフルな終了とクリーンアップ

trapコマンドを使用すると、スクリプトはシグナル(Ctrl+C、システム終了、スクリプト終了など)をキャッチし、終了前に指定されたコマンドまたは関数を実行できます。これはクリーンアップタスクに不可欠です。

cleanup関数

変更を元に戻す(例:一時ファイルの削除、設定のリセット)専用の関数を定義し、trapを使用してスクリプトの終了方法に関係なく実行されるようにします。

# クリーンアップ関数がチェックするためのグローバル変数
TEMP_FILE=""

cleanup() {
    printf '%s\n' "--- Running Cleanup Procedures ---"
    if [[ -f "$TEMP_FILE" ]]; then
        rm -f "$TEMP_FILE"
        echo "Deleted temporary file: $TEMP_FILE"
    fi
    # オプションで、最終終了ステータスレポートを提供
}

# 1. EXITをトラップ:成功、失敗、シグナルに関わらずクリーンアップを実行
trap cleanup EXIT

# 2. シグナルをトラップ(INT=Ctrl+C、TERM=Killシグナル)
trap 'printf "%s\n" "Script interrupted by user or system signal." >&2; exit 130' INT
trap 'printf "%s\n" "Script terminated." >&2; exit 143' TERM

# --- メインスクリプトロジック ---
TEMP_FILE=$(mktemp)
echo "Temporary content" > "$TEMP_FILE"
# ここでスクリプトが失敗または中断された場合、cleanup()が実行されることが保証されます

trap cleanup EXITを使用する理由

EXITtrapを設定すると、スクリプトが正常に終了するか(exit 0)、明示的にエラーで終了するか(exit 1)、set -eのために強制終了されるかに関わらず、クリーンアップ関数が実行されることが保証されます。

高度なエラーレポート

標準のエラーメッセージ(command not found)は、コンテキストを欠いていることがよくあります。高度なスクリプトは、何が失敗したか、どこで失敗したか、なぜ失敗したかを報告する必要があります。

行番号のログ記録

error_exitのような関数が呼び出された場合、BASH_LINENO配列またはcallerコマンド(ただしcallerは関数内に制限されることが多い)を使用して、エラーが発生したスクリプト内の行番号を特定できます。

関数の外部での単純なレポートには、LINENO変数を使用します:

# 即時失敗レポートの例
(some_risky_command) || {
    echo "[ERROR $LINENO] some_risky_command failed with status $?" >&2
    exit 3
}

出力の区別

情報メッセージは常に標準出力(stdout)に送信し、エラー/警告メッセージは標準エラー出力(stderr)に送信します。これは、スクリプトの出力が別のプログラムにパイプされたり、外部にログ記録されたりする場合に重要です。

  • echo "Informational message"stdoutに送信)
  • echo "[WARNING] Configuration override" >&2stderrに送信)

パターンをまとめる

ほとんどの本番スクリプトでは、実用的なパターンはシンプルです:set -euo pipefailで開始し、作業を行う前に入力を検証し、予想される失敗をif ! command; then ...; fiでラップし、一時的な状態を作成する前にtrap cleanup EXITを追加します。

これにより、謎の失敗ではなく、有用な失敗が得られます。次回、午前2時にジョブが壊れた場合、ログには何が失敗したか、どこを確認すべきか、クリーンアップが実行されたかどうかが表示されるはずです。