Bashスクリプトにおける効果的なエラーハンドリング戦略
厳格モード、トラップ、終了コード、明確なstderrメッセージを使用して、Bashスクリプトを安全に失敗させ、後片付けを行えるようにします。
Bashスクリプトにおける効果的なエラーハンドリング戦略
Bashスクリプトのエラーハンドリングは重要です。なぜなら、スクリプトが静かに失敗すると、ファイルの一部だけがコピーされたり、壊れたコードがデプロイされたり、間違ったパスが削除されたりする可能性があるからです。重要なステップが失敗したらスクリプトを停止し、何が起こったのかを説明し、終了する前に一時ファイルをクリーンアップする必要があります。
以下のパターンは、最も頻繁に必要となる要素をカバーしています:厳格モード、明示的なチェック、trap、そしてシンプルなエラー報告です。
基礎:終了ステータスを理解する
Unixの世界では、実行されたすべてのコマンドは終了ステータス(終了コード)を返します。これは操作の結果を示す整数値で、特別な変数$?に即座に格納されます。
- 終了コード0: 慣例により、これは成功(または'true')を意味します。
- 終了コード1–255: これらは失敗(または'false')を意味します。特定のコードは特定の種類の失敗に関連することがよくあります(例:1は一般的なエラー、127はコマンドが見つからない)。
信頼性の高いスクリプトは、重要なコマンドの終了ステータスをチェックし、スクリプトが失敗した場合には意味のある0以外のコードを返す必要があります。
コア戦略1:防御的スクリプティングの三種の神器
本格的な自動化スクリプトでは、シバン行(#!/bin/bash)の直後に3つの基本的なオプションを適用することから始めるべきです。これらのオプションは、厳格で予測可能な動作を強制します。
1. 失敗時の即時終了(set -e)
set -eオプション(またはset -o errexit)は、コマンドが失敗した場合(0以外の終了ステータスを返した場合)、スクリプトが即座に終了することを指示します。
これは「フェイルファスト」の原則と呼ばれることが多く、不完全または失敗した前提条件の結果を使用して、スクリプトが潜在的に破壊的なアクションを続行するのを防ぎます。
#!/bin/bash
set -e
echo "プロセスを開始します..."
mkdir /tmp/test_dir
cp non_existent_file /tmp/test_dir/ # このコマンドは失敗します(終了コード > 0)
echo "この行は実行されません。" # スクリプトはここで終了します
警告:
set -eの注意点
set -eは、ifやwhileでテストされるコマンド、ほとんどの&&や||リスト内のコマンド、!でステータスが反転されたコマンドなど、いくつかの一般的なコンテキストでは終了をトリガーしません。これは安全網として扱い、予期される失敗の周りでの明確なチェックの代わりにはなりません。
2. 未設定変数をエラーとして扱う(set -u)
set -uオプション(またはset -o nounset)は、未設定の変数の使用をエラーとして扱い、スクリプトを即座に終了させます(set -eと同様)。これにより、変数名のタイプミスが空文字列として重要なコマンドに渡されるという微妙なバグを防ぎます。
#!/bin/bash
set -u
# echo "変数は: $UNDEFINED_VAR" # スクリプトはここで失敗し終了します
MY_VAR="defined"
echo "変数は: ${MY_VAR}"
3. コマンドパイプラインの処理(set -o pipefail)
デフォルトでは、コマンドパイプライン(command1 | command2 | command3)は最後のコマンド(command3)の終了ステータスのみを報告します。command1が失敗してもcommand3が成功した場合、$?は0になり、失敗が隠蔽されます。
set -o pipefailはこの動作を変更し、パイプライン内のいずれかのコマンドが失敗した場合、パイプラインが0以外のステータスを返すようにします。これは信頼性の高いデータ処理に不可欠です。
#!/bin/bash
set -o pipefail
# コマンド `false` は常に1で終了します
# pipefailがない場合、この行は `cat` が成功するため0を返します。
false | cat # pipefailにより1を返します
if [ $? -ne 0 ]; then
echo "パイプラインが失敗しました。"
fi
ベストプラクティス:ヘッダー
堅牢なスクリプトは常に、防御オプションを組み合わせて開始します:
#!/bin/bash set -euo pipefail
コア戦略2:手動チェックと条件付き実行
set -eはほとんどの失敗を処理しますが、特に失敗が予期されていたり、特定のログ記録が必要な場合には、コマンドのステータスを手動でチェックする必要がよくあります。
if文によるチェック
コマンドの成功をチェックする標準的な方法は、ifブロック内でその終了ステータスをキャプチャすることです。この方法はset -eの動作をオーバーライドし、エラーを明示的に処理できるようにします。
#!/bin/bash
set -euo pipefail
TEMP_FILE="/tmp/data_processing_$$/config.dat"
# ディレクトリの作成を試み、失敗を明示的に処理
if ! mkdir -p "$(dirname "$TEMP_FILE")"; then
echo "[ERROR] 一時ディレクトリを作成できませんでした。" >&2
exit 1
fi
# データの取得を試みる
if ! curl -sSf https://api.example.com/data > "$TEMP_FILE"; then
echo "[ERROR] APIからのデータ取得に失敗しました。" >&2
exit 2
fi
echo "データが正常に取得されました。"
ヒント:
curlの-sSfフラグ(サイレント、失敗、エラー表示)は、HTTPエラー時にcurlが0以外の終了コードを返すように強制し、エラー処理を容易にします。
ショートサーキット演算子の使用(&& と ||)
これらの論理演算子は、成功(&&)または失敗(||)に基づいてコマンドをチェーンする簡潔な方法を提供します。
command1 && command2:command1が成功した場合のみcommand2を実行します。command1 || command2:command1が失敗した場合のみcommand2を実行します。
# 例:ディレクトリを作成し、ファイルをコピー。いずれかのステップが失敗したら失敗
mkdir logs && cp /var/log/syslog logs/system.log
# 例:バックアップを試み、失敗した場合はエラーをログに記録して終了
pg_dump database > backup.sql || { echo "バックアップに失敗しました!" >&2; exit 10; }
高度な戦略3:trapによる確実なクリーンアップ
スクリプトが一時ファイル、ロックファイル、確立されたネットワーク接続を扱う場合、突然の終了(成功によるものもエラーによるものも)はシステムを不整合な状態のままにする可能性があります。trapコマンドを使用すると、スクリプトが特定のシグナルを受信したときに実行されるコマンドまたは関数を定義できます。
EXITシグナル
EXITシグナルは、一般的なクリーンアップに最も便利です。トラップされたコマンドは、スクリプトが終了するとき(成功した終了、手動のexit呼び出し、set -eによってトリガーされた終了のいずれであっても)常に実行されます。
#!/bin/bash
TEMP_DIR=$(mktemp -d)
# クリーンアップ関数の定義
cleanup() {
EXIT_CODE=$?
echo "一時ディレクトリをクリーンアップ中: ${TEMP_DIR}"
rm -rf "$TEMP_DIR"
# スクリプトが失敗により終了した場合、失敗コードを復元
if [ $EXIT_CODE -ne 0 ]; then
exit $EXIT_CODE
fi
}
# トラップを設定:スクリプト終了時に'cleanup'関数を実行
trap cleanup EXIT
# --- メインスクリプトロジック ---
echo "${TEMP_DIR} でデータを処理中"
# 成功する操作をシミュレート...
# ... スクリプト続行 ...
# set -eをトリガーする重大な失敗をシミュレート
false
# この行には到達しませんが、クリーンアップは確実に実行されます。
echo "完了。"
特定のシグナルの処理(TERM、INT)
TERM(終了要求)やINT(割り込み、多くの場合Ctrl+C)などの特定の終了シグナルをトラップして、ユーザーやスケジューラーがジョブをキャンセルした場合にグレースフルシャットダウンを保証することもできます。
trap 'echo "ユーザー(Ctrl+C)によりスクリプトが中断されました。クリーンアップを中止します。" >&2; exit 130' INT
戦略4:カスタムエラー報告とログ記録
プロフェッショナルなスクリプトは、専用のエラー関数を使用して報告を集中化し、一貫性と適切な出力チャネルを確保する必要があります。
エラーを標準エラーにリダイレクトする(>&2)
エラーメッセージは常に標準エラー(stderr、ファイル記述子2)に出力し、標準出力(stdout、ファイル記述子1)をデータや成功結果のためにクリーンに保つ必要があります。
die関数パターン
dieやerror_exitとよく名付けられる関数を作成し、メッセージのログ記録、クリーンアップ(トラップが使用されていない場合)、指定されたコードでの終了を処理します。
# エラーメッセージを出力して終了する関数
die() {
local msg=$1
local code=${2:-1}
echo "$(date +'%Y-%m-%d %H:%M:%S') [FATAL]: ${msg}" >&2
exit "$code"
}
# 使用例:
REQUIRED_VAR="$1"
if [ -z "$REQUIRED_VAR" ]; then
die "必須引数(データベース名)が不足しています。" 3
fi
# ... スクリプト内で後ほど ...
if ! validate_checksum "$FILE"; then
die "$FILE のチェックサム検証に失敗しました。" 5
fi
失敗を退屈なものに
信頼性の高いBashスクリプトのエラーハンドリングのために、重要なスクリプトはset -euo pipefailで開始し、コマンドが失敗する可能性がある場所ではif ! command; then ...; fiを使用し、エラーはstderrに送信します。スクリプトが一時ファイル、ロックファイル、部分的な出力を作成する場合は、リスクのある作業を開始する前にtrap cleanup EXITを追加します。
この組み合わせにより、小さな自動化タスクを予測可能に保ち、本番環境での障害を診断しやすくします。