Bashスクリプティング:終了コードとステータスの深掘り

Bashの終了コードを理解し、$?を安全に確認し、exitでステータスを設定し、信頼性の高い制御フローを構築します。

Bashスクリプティング:終了コードとステータスの深掘り

Bashの終了コードは、コマンドがスクリプトに何が起こったかを伝える方法です。0は成功を意味し、ゼロ以外のステータスはコマンドが失敗したか、スクリプトが処理する必要のある結果を生成したことを示します。

このガイドでは、$?の読み方、exitによるステータスの設定、および終了コードを使用してBash自動化でより安全な制御フローを構築する方法を示します。

終了コードを理解する

Bashで実行されるすべてのコマンド、関数、またはスクリプトは、完了時に終了コードを返します。これは、実行の結果を示す整数値です。慣例により:

  • 0(ゼロ)成功を示します。コマンドはエラーなく完了しました。
  • 非ゼロ(その他の整数)失敗またはエラーを示します。異なる非ゼロ値は、特定の種類のエラーを示す場合があります。

この単純な0非ゼロの慣例は、Bashの動作方法と、スクリプトに条件付きロジックを構築する方法の基本です。

最後の終了コードを取得する:$?

Bashは特別なパラメータ$?を提供します。これは、最後に実行されたフォアグラウンドコマンドの終了コードを保持します。任意のコマンドの直後にその値をチェックして、結果を判断できます。

# 例1:成功したコマンド
ls /tmp
echo "'ls /tmp'の終了コード:$?"

# 例2:失敗したコマンド(存在しないディレクトリ)
ls /nonexistent_directory
echo "'ls /nonexistent_directory'の終了コード:$?"

# 例3:一致を見つけるgrep(成功)
grep "root" /etc/passwd
echo "'grep root /etc/passwd'の終了コード:$?"

# 例4:一致を見つけないgrep(失敗だが予想される)
grep "nonexistent_user" /etc/passwd
echo "'grep nonexistent_user /etc/passwd'の終了コード:$?"

出力(システムと/etc/passwdの内容によって異なる場合があります):

ls /tmp
# ...(/tmp内のファイル一覧)
'ls /tmp'の終了コード:0
ls /nonexistent_directory
ls: '/nonexistent_directory'にアクセスできません: そのようなファイルやディレクトリはありません
'ls /nonexistent_directory'の終了コード:2
grep "root" /etc/passwd
root:x:0:0:root:/root:/bin/bash
'grep root /etc/passwd'の終了コード:0
grep "nonexistent_user" /etc/passwd
'grep nonexistent_user /etc/passwd'の終了コード:1

grepは一致に対して0を返し、一致なしに対して1を返すことに注意してください。どちらもgrepのコンテキストでは有効な結果ですが、条件付きロジックでは、0はパターンの正常な発見を示します。

exitで終了コードを明示的に設定する

独自のスクリプトや関数を作成する場合、exitコマンドの後に整数値を指定して、終了コードを明示的に設定できます。これは、スクリプトの結果を呼び出し元のプロセス、親スクリプト、またはCI/CDパイプラインに伝えるために重要です。

#!/bin/bash

# script_success.sh
echo "このスクリプトは成功(0)で終了します"
exit 0
#!/bin/bash

# script_failure.sh
echo "このスクリプトは失敗(1)で終了します"
exit 1
# スクリプトをテストする
./script_success.sh
echo "script_success.shのステータス:$?"

./script_failure.sh
echo "script_failure.shのステータス:$?"

出力:

このスクリプトは成功(0)で終了します
script_success.shのステータス:0
このスクリプトは失敗(1)で終了します
script_failure.shのステータス:1

ヒント: exitが引数なしで呼び出された場合、スクリプトの終了ステータスは、exitが呼び出される前に最後に実行されたコマンドの終了ステータスになります。

制御フローのための終了コードの活用

終了コードはBashにおける条件付き実行の基盤であり、動的で応答性の高いスクリプトを作成できます。

条件文(if/else

Bashのif文は、コマンドの終了コードを評価します。コマンドが0(成功)で終了した場合、ifブロックが実行されます。それ以外の場合は、elseブロック(存在する場合)が実行されます。

#!/bin/bash

FILE="/path/to/my/important_file.txt"

if [ -f "$FILE" ]; then # テストコマンド`[`はファイルが存在すれば0で終了
    echo "ファイル'$FILE'が存在します。処理を続行します..."
    # ここにファイル処理ロジックを追加
    # 例:cat "$FILE"
    exit 0
else
    echo "エラー:ファイル'$FILE'が存在しません。"
    echo "スクリプトを中止します。"
    exit 1
fi

論理演算子(&&||

Bashは、終了コードに依存する強力な短絡論理演算子を提供します:

  • command1 && command2command10(成功)で終了した場合にのみcommand2が実行されます。
  • command1 || command2command1非ゼロ値(失敗)で終了した場合にのみcommand2が実行されます。

これらは、連続したコマンドやフォールバックメカニズムに非常に便利です。

#!/bin/bash

LOG_DIR="/var/log/my_app"

# ディレクトリが存在しない場合のみ作成
mkdir -p "$LOG_DIR" && echo "ログディレクトリ'$LOG_DIR'を確認しました。"

# サービスを起動し、失敗した場合はフォールバックコマンドを試す
systemctl start my_service || { echo "my_serviceの起動に失敗しました。フォールバックを試みます..."; ./start_fallback.sh; }

# スクリプトを続行するために成功しなければならないコマンド
copy_data_to_backup_location && echo "データのバックアップに成功しました。" || { echo "データのバックアップに失敗しました!"; exit 1; }

echo "スクリプトが正常に完了しました。"
exit 0

set -e:エラーで終了

set -eオプションは、スクリプトをより堅牢にするための強力なツールです。set -eがアクティブな場合、Bashはコマンドが非ゼロのステータスで終了するとすぐにスクリプトを終了します。これにより、サイレント障害や連鎖エラーを防ぎます。

#!/bin/bash
set -e # コマンドが非ゼロのステータスで終了した場合、すぐに終了する

echo "スクリプトを開始します..."

# このコマンドは成功します
ls /tmp

echo "最初のコマンドは成功しました。"

# このコマンドは失敗し、'set -e'によりスクリプトはここで終了します
ls /nonexistent_path

echo "前のコマンドが失敗した場合、この行は決して実行されません。"

exit 0 # この行は、先行するすべてのコマンドが成功した場合にのみ実行されます

出力(/nonexistent_pathが存在しない場合):

スクリプトを開始します...
# ...(ls /tmpの出力)
最初のコマンドは成功しました。
ls: '/nonexistent_path'にアクセスできません: そのようなファイルやディレクトリはありません

スクリプトは失敗したlsコマンドの後に終了し、「この行は決して実行されません」というメッセージは出力されません。

警告: set -eには例外があり、一部のコマンドは予想される結果に対して非ゼロを返すことがあります。例えば、grepは一致が見つからない場合に1を返します。結果を気にする場合は、明示的にif grep -q "pattern" file; then ... fiを使用してください。

一般的な終了コードのシナリオとベストプラクティス

成功には0、失敗には非ゼロが一般的なルールですが、特にシステムコマンドや組み込みコマンドでは、一部の非ゼロコードに共通の意味があります:

  • 0:成功。
  • 1:一般的なエラー、さまざまな問題のキャッチオール。
  • 2:シェル組み込みコマンドの誤用、または不正なコマンド引数。
  • 126:呼び出されたコマンドを実行できない(例:権限の問題、実行可能ファイルではない)。
  • 127:コマンドが見つからない(例:コマンド名のタイプミス、PATHにない)。
  • 128 + N:コマンドがシグナルNで終了されました。例えば、130(128 + 2)は、コマンドがSIGINT(Ctrl+C)で終了されたことを意味します。

独自のスクリプトを作成する場合は、成功には0を使用してください。失敗には、一般的なエラーとして1が安全なデフォルトです。スクリプトが複数の異なるエラー条件を処理する場合は、より高い非ゼロ値(例:102030)を使用してそれらを区別できますが、これらのカスタムコードを明確に文書化してください

堅牢なスクリプトのためのベストプラクティス:

  1. 重要なコマンドを常にチェックする:成功を想定しないでください。if文または&&を使用して重要なステップを確認します。
  2. 情報を提供するエラーメッセージを表示する:スクリプトが失敗した場合、何が問題で、どのように修正できるかを説明する明確なメッセージをstderrに出力します。出力を標準エラーにリダイレクトするには>&2を使用します。
    my_command || { echo "エラー:my_commandが失敗しました。ログを確認してください。" >&2; exit 1; }
    
  3. 失敗時にクリーンアップするtrapを使用して、スクリプトが途中で終了した場合でも、一時ファイルやリソースが確実にクリーンアップされるようにします。
    cleanup() {
        echo "一時ファイルをクリーンアップしています..."
        rm -f /tmp/my_temp_file_$$
    }
    trap cleanup EXIT
    
  4. 入力を検証する:スクリプトの引数や環境変数を早期にチェックし、無効な場合は情報を提供するエラーで終了します。
  5. 終了ステータスをログに記録する:複雑な自動化では、監査とデバッグのために主要な操作の終了ステータスをログに記録します。

実世界の例:堅牢なバックアップスクリプトのスニペット

実際のシナリオでこれらの概念をどのように組み合わせるかを示します:

#!/bin/bash
set -e # コマンドが非ゼロのステータスで終了した場合、すぐに終了する

BACKUP_SOURCE="/data/app/config"
BACKUP_DEST="/mnt/backup/configs"
TIMESTAMP=$(date +%Y%m%d%H%M%S)
LOG_FILE="/var/log/backup_config_${TIMESTAMP}.log"

# --- 関数 ---
log_message() {
    echo "$(date +%Y-%m-%d_%H:%M:%S) - $1" | tee -a "$LOG_FILE"
}

cleanup() {
    log_message "クリーンアップを開始しました。"
    if [ -n "${TEMP_DIR:-}" ] && [ -d "$TEMP_DIR" ]; then
        rm -rf "$TEMP_DIR"
        log_message "一時ディレクトリを削除しました:$TEMP_DIR"
    fi
}

# --- 終了とシグナルのトラップ ---
trap 'cleanup' EXIT
trap 'log_message "スクリプトが中断されました(SIGINT)。終了します。"; exit 130' INT
trap 'log_message "スクリプトが終了されました(SIGTERM)。終了します。"; exit 143' TERM

# --- メインスクリプトロジック ---
log_message "設定のバックアップを開始します。"

# 1. ソースディレクトリが存在するか確認
if [ ! -d "$BACKUP_SOURCE" ]; then
    log_message "エラー:バックアップソース'$BACKUP_SOURCE'が存在しません。" >&2
    exit 2 # 無効なソースのカスタムエラーコード
fi

# 2. バックアップ先が存在することを確認
mkdir -p "$BACKUP_DEST" || {
    log_message "エラー:バックアップ先'$BACKUP_DEST'の作成/確認に失敗しました。" >&2
    exit 3 # 宛先の問題のカスタムエラーコード
}

# 3. 圧縮用の一時ディレクトリを作成
TEMP_DIR=$(mktemp -d)
log_message "一時ディレクトリを作成しました:$TEMP_DIR"

# 4. データを一時ディレクトリにコピー
cp -r "$BACKUP_SOURCE" "$TEMP_DIR/" || {
    log_message "エラー:'$BACKUP_SOURCE'から'$TEMP_DIR'へのデータコピーに失敗しました。" >&2
    exit 4 # コピー失敗のカスタムエラーコード
}
log_message "データを一時的な場所にコピーしました。"

# 5. データを圧縮
ARCHIVE_NAME="config_backup_${TIMESTAMP}.tar.gz"
tar -czf "$TEMP_DIR/$ARCHIVE_NAME" -C "$TEMP_DIR" "$(basename "$BACKUP_SOURCE")" || {
    log_message "エラー:データの圧縮に失敗しました。" >&2
    exit 5 # 圧縮失敗のカスタムエラーコード
}
log_message "データを$ARCHIVE_NAMEに圧縮しました。"

# 6. アーカイブを最終的な宛先に移動
mv "$TEMP_DIR/$ARCHIVE_NAME" "$BACKUP_DEST/" || {
    log_message "エラー:アーカイブを'$BACKUP_DEST'に移動できませんでした。" >&2
    exit 6 # 移動失敗のカスタムエラーコード
}
log_message "アーカイブを'$BACKUP_DEST/$ARCHIVE_NAME'に移動しました。"

log_message "バックアップが正常に完了しました!"
exit 0

まとめ

終了コードをスクリプトのインターフェースの一部として扱ってください。重要なコマンドをチェックし、失敗時に明確な非ゼロステータスを返し、別のスクリプトやCIジョブが解釈する必要があるカスタムコードを文書化してください。