終了コードの理解:$? と exit を使った効果的なエラーハンドリング

Bashの終了コード、$?、exit、set -e、pipefailを使って、スクリプトの失敗を明確かつ制御可能にします。

終了コードの理解:$? と exit を使った効果的なエラーハンドリング

Bashスクリプトが失敗したとき、終了コードは呼び出し元に次に何をすべきかを伝えます:続行、再試行、警告、または停止。終了コード、$?exitを理解することは、失敗を隠す自動化と、それを明確に報告する自動化の違いを生みます。

このガイドでは、Bashがコマンドのステータスをどのように追跡するか、そしてそのステータスをシンプルで信頼性の高いエラーハンドリングにどのように使用できるかを示します。

終了ステータスの概念

Unix系シェル環境で実行されるすべてのコマンドやプログラム(cdのような組み込みコマンド、grepのような外部ユーティリティ、他のシェルスクリプトなど)は、完了時に整数値を返します。この整数が終了コードであり、操作の結果を呼び出し元のプロセスに通知します。

標準的な慣例

終了コードの慣例は広く認識されています:

  • 0(ゼロ): 成功を示します。コマンドは期待通りに実行され、エラーは発生しませんでした。
  • 1から255: 失敗または特定のエラー状態を示します。これらの非ゼロの値は、何か問題が発生したことを示します。数値が大きいほど、特定の種類のエラー(例:ファイルが見つからない、権限がない、構文エラー)に対応することがよくありますが、正確な意味は特定のプログラムに依存します。

範囲に関する注意: 終了コードは技術的には8ビット値(0〜255)ですが、シェルスクリプトは通常、成功の場合は0、失敗の場合は非ゼロのみを気にします。255より大きい終了コードは、通常、シェルによって切り捨てられるか、256で割った余りとして解釈されます。

最後の終了コードを検査する:$? 変数

特別なシェル変数 $?(ドル疑問符)は、コマンドのステータスを監視するための中心的な役割を果たします。任意のコマンドが実行された直後に、シェルはその終了コードを $? に格納します。

$? の使い方

$? は、関心のあるコマンドの直後にチェックする必要があります。後続のコマンド(変数をエコーすることさえも)がその値を上書きするためです。

例1:成功と失敗のチェック

# 1. 成功するコマンド
echo "Success test" > /dev/null
echo "Exit code for success: $?"

# 2. 失敗するコマンド(例:存在しないファイルをリストしようとする)
ls /non/existent/path
echo "Exit code for failure: $?"

期待される出力:

Exit code for success: 0
ls: cannot access '/non/existent/path': No such file or directory
Exit code for failure: 2

条件付きエラーチェックの実装

単に終了コードを知るだけでは十分ではありません。真の力は、この情報を使用してスクリプトのフローを制御することにあります。これは通常、if 文またはショートサーキット演算子(&&||)を使用して行われます。

if 文の使用

これはエラーを処理する最も明示的な方法です:

if grep -q "important data" logfile.txt;
then
    echo "Data found successfully."
else
    LAST_STATUS=$?
    echo "Error: Grep failed with status $LAST_STATUS. Data not found."
    # スクリプトが続行できない場合は、ここで終了することを検討してください
fi

上記の例では、grep -q は出力を抑制し(-q)、一致が見つかった場合のみ0を返します。if 構造は自動的に終了ステータスをチェックしますが、else ブロック内で $? を明示的にキャプチャすることは、詳細なログ記録に役立ちます。

ショートサーキットロジックの使用(&&||

単純な順次チェックの場合、ショートサーキット演算子は簡潔なエラーハンドリングを提供します:

  • &&(AND): && の後のコマンドは、前のコマンドが成功した(0を返した)場合にのみ実行されます。
  • ||(OR): || の後のコマンドは、前のコマンドが失敗した(非ゼロを返した)場合にのみ実行されます。

例2:簡潔なエラーハンドリング

# 1. 'fetch_data' が成功した場合のみ 'process_data' を実行
fetch_data.sh && ./process_data.sh

# 2. プライマリ操作が失敗した場合のみ 'send_alert' を実行
rsync -a source/ dest/ || echo "RSync failed on $(date)" >> /var/log/rsync_errors.log

exit によるスクリプト終了の制御

exit コマンドは、現在のシェルスクリプトまたは関数を即座に終了し、指定された終了ステータスを呼び出し元(別のスクリプトまたはユーザーのターミナルである可能性があります)に返すために使用されます。

構文と使用法

構文は単に exit [status_code] です。

ステータスが指定されていない場合、exit はデフォルトで最後に実行されたフォアグラウンドコマンドのステータスになります。最初にコマンドを実行せずに明示的に exit 0 を呼び出すと、0を返します。

例3:前提条件の失敗で終了する

このスクリプトは、処理を進める前に必要な設定ファイルが存在することを確認します。

CONFIG_FILE="/etc/app/config.conf"

if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "Error: Configuration file not found at $CONFIG_FILE."
    # 特定のエラーコード(例:20)でスクリプトを即座に終了
    exit 20 
fi

echo "Configuration loaded. Continuing script..."
# ... スクリプトの残り
exit 0

ベストプラクティス:意味のある終了コードの使用

01 はほとんどの基本的なケースをカバーしますが、異なる非ゼロコードを使用することで、呼び出し元のスクリプトが正確な問題を診断するのに役立ちます:

コード 意味(例)
0 成功
1 一般的なキャッチオールエラー
2-10 構文エラー、引数解析の問題
20 前提条件の欠落(例:ファイルが見つからない)
30 権限の問題

スクリプトを迅速に失敗させる:set コマンド

複雑なスクリプトで最大限の信頼性を得るには、スクリプトの先頭で set コマンドオプションを使用してエラーチェックをグローバルに有効にすることが強く推奨されます:

#!/bin/bash

# コマンドが非ゼロのステータスで終了した場合、即座に終了します。
set -e

# 未設定の変数を置換時にエラーとして扱います。
set -u

# Pipefail:パイプラインの戻りステータスが、非ゼロで終了した右端のコマンドのステータスになるようにします。
set -o pipefail

# (オプションですが便利)デバッグのために実行されたコマンドを出力します
# set -x 

# 以下のコマンドのいずれかが失敗した場合、スクリプトは即座に停止します。
ls /valid/path && grep pattern file.txt && ./next_step.sh

# 次の行は、先行するすべてのコマンドが成功した場合にのみ実行されます。
echo "All steps complete."

set -e がアクティブな場合、多くの未処理の非ゼロステータスは、後のコマンドが誤った前提で実行される前にスクリプトを停止します。ただし、条件文、パイプライン、複合コマンドには例外があるため、予想される失敗は明示的に処理してください。

たとえば、grep は一致が見つからない場合に 1 を返します。これは致命的なエラーではなく、正常な結果である可能性があります:

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

まとめ

重要なコマンドは実行時にチェックし、エラーはstderrに書き込み、スクリプトが安全に続行できない場合は非ゼロのステータスで終了します。迅速に失敗するスクリプトには set -euo pipefail を使用しますが、それを唯一のエラーハンドリング戦略として依存しないでください。