閉じる

Bashで前のコマンドが失敗した時に実行する方法|||演算子とif文によるエラー制御

Bashスクリプトを利用してサーバー運用や定型業務を自動化する際、避けて通れないのが「エラーハンドリング」です。

コマンドが常に成功することを前提にスクリプトを組んでしまうと、予期せぬ失敗が発生した際に後続の処理が誤った状態で実行され、データの破損やシステムの不整合を引き起こすリスクがあります。

本記事では、Bashで前のコマンドが失敗した時に特定の処理を実行する方法について、初心者から中級者まで活用できる手法を詳しく解説します。

短く記述できる || 演算子から、複雑な条件分岐を可能にする if 文、さらにはスクリプト全体のエラー監視まで、現場で即戦力となるテクニックを身につけていきましょう。

Bashにおけるコマンドの成功と失敗の定義

Bashでエラーハンドリングを行うための大前提として、まずは「コマンドが成功したか失敗したかをどのように判断しているか」を理解する必要があります。

終了ステータス(Exit Status)とは

すべてのBashコマンドは、実行終了時に 終了ステータス(Exit Status) と呼ばれる数値を返します。

この数値は、直前のコマンドの結果を保持する特殊変数 $? を参照することで確認できます。

終了ステータスには以下のルールがあります。

終了ステータスの値意味
0成功(Success)
1 ~ 255失敗(Failure)

例えば、存在しないディレクトリに移動しようとした場合、コマンドは失敗し、0以外の数値が返されます。

Shell
# 存在しないディレクトリへ移動を試みる
cd /non_existent_directory
# 終了ステータスを表示
echo $?
Shell
bash: cd: /non_existent_directory: No such file or directory
1

このように、Bashは 「0であれば正常終了、それ以外は異常終了」 と判定します。

この仕組みを利用して、エラー時の挙動を制御していきます。

|| 演算子による簡潔なエラー制御

最もシンプルかつ頻繁に使われる手法が、OR演算子(||) を使用する方法です。

この演算子を使うと、左側のコマンドが失敗したときのみ、右側のコマンドを実行させることができます。

基本的な使い方

構文は以下の通りです。

Shell
コマンド1 || コマンド2

この一行は、「コマンド1を実行し、もし失敗(終了ステータスが0以外)したらコマンド2を実行する」という論理構造を持っています。

具体的な活用例

例えば、バックアップディレクトリを作成し、もし作成に失敗した場合にメッセージを表示してスクリプトを中断(exit)させる処理は、次のように書けます。

Shell
# ディレクトリ作成。失敗したらメッセージを出して終了。
mkdir /backup_dir || echo "ディレクトリの作成に失敗しました。"

より実務に近い形では、以下のように exit コマンドと組み合わせて、異常時に処理を止める使い方が一般的です。

Shell
# 重要なディレクトリへの移動。失敗したらスクリプト自体を終了させる。
cd /var/www/html || { echo "ディレクトリが見つかりません。処理を中断します。"; exit 1; }

実行結果(ディレクトリが存在しない場合):

Shell
bash: cd: /var/www/html: No such file or directory
ディレクトリが見つかりません。処理を中断します。

ここで、複数のコマンドを { ...; } で囲っている点に注目してください。

これにより、エラー発生時に「メッセージ出力」と「スクリプト終了」の2つの動作を同時に実行できます。

if文を用いた詳細なエラー制御

一行で書ける || は便利ですが、複雑なリカバリ処理を行いたい場合や、コードの可読性を重視する場合は if文 を使用するのが最適です。

if文で終了ステータスを直接判定する

Bashの if 文は、指定されたコマンドの終了ステータスが0かどうかを直接評価できます。

Shell
if コマンド; then
    # 成功時の処理
else
    # 失敗時の処理
fi

これを「失敗したとき」にフォーカスして書くには、論理否定の ! を使うのがスマートです。

Shell
# コマンドの前に ! を付ける
if ! cp source.txt destination.txt; then
    echo "コピーに失敗しました。ディスク容量を確認してください。"
    # ここにリカバリ処理を記述できる
fi

$? 変数を利用した分岐

特定の終了ステータスに応じて処理を変えたい場合は、変数 $? を明示的に使用します。

Shell
# ファイルのダウンロードを試行
curl -O https://example.com/data.zip

# 終了ステータスを変数に格納
res=$?

if [ $res -ne 0 ]; then
    echo "ダウンロード中にエラーが発生しました。エラーコード: $res"
    case $res in
        6) echo "ホストを解決できませんでした。";;
        7) echo "ホストに接続できませんでした。";;
        *) echo "未知のエラーです。";;
    esac
    exit 1
fi

注意点として、$?「直前のコマンド」の結果のみ を保持します。

エラー判定の前に別のコマンド(例えば echo など)を挟んでしまうと、$? の中身が書き換わってしまうため、上記のように変数に退避させるのが安全です。

実践的なユースケース:ログ記録と通知

実際の運用現場では、エラーが発生した際に単に止まるだけでなく、ログファイルへの記録や管理者への通知を行う必要があります。

ログファイルへのエラー出力

標準エラー出力(stderr)をファイルにリダイレクトしつつ、失敗時にカスタムメッセージをログに残す例を紹介します。

Shell
LOG_FILE="/var/log/myscript.log"

# コマンド実行。エラーはログファイルへ。
ls /root/secret_files 2>> "$LOG_FILE" || {
    echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] アクセス権限がないか、ディレクトリが存在しません。" >> "$LOG_FILE"
}

エラーハンドリング関数の共通化

スクリプト内で何度も同じようなエラーチェックを行う場合、関数化 することでコードをスッキリさせることができます。

Shell
# エラーハンドリング用関数
error_exit() {
    local message=$1
    echo "$(date '+%Y-%m-%d %H:%M:%S') [CRITICAL]: $message" >&2
    # ここで通知メール送信などの処理を追加可能
    exit 1
}

# 関数の呼び出し
cd /tmp/work_dir || error_exit "作業ディレクトリへの移動に失敗しました。"
tar -czf backup.tar.gz . || error_exit "圧縮処理に失敗しました。"

このように関数にまとめておけば、エラー時の挙動(ログの書式や終了コードなど)を一括で管理できるため、メンテナンス性が大幅に向上します。

発展:trapコマンドによるエラー捕捉

個別のコマンドごとにエラーチェックを書くのが困難な大規模なスクリプトでは、trap コマンドを用いた シグナル捕捉 が有効です。

ERRシグナルの活用

Bashには、いずれかのコマンドが失敗した際に自動的に特定の処理を実行する trap ERR という仕組みがあります。

Shell
# エラーが発生した際に実行する関数
on_error() {
    echo "エラーが発生しました。スクリプトの $1 行目付近を確認してください。"
}

# ERRシグナルを捕捉するように設定
trap 'on_error $LINENO' ERR

# 失敗するコマンドの例
cat /etc/shadow # 一般ユーザーで実行すると権限エラーになる
Shell
cat: /etc/shadow: Permission denied
エラーが発生しました。スクリプトの 9 行目付近を確認してください。

trap を使うことで、スクリプトのどこでエラーが起きても漏らさずにキャッチできるようになります。

ただし、意図的に失敗を許容したいコマンドがある場合には注意が必要です。

エラー制御における注意点とベストプラクティス

Bashのエラー処理を実装する上で、陥りやすい罠がいくつかあります。

set -e(errexit)の功罪

スクリプトの冒頭に set -e を記述すると、コマンドが一つでも失敗した時点でスクリプトを即座に終了させることができます。

非常に便利な機能ですが、パイプラインの中での失敗 を検知できなかったり、意図的にエラーを無視したい箇所で挙動が複雑になったりするデメリットもあります。

モダンな開発では、set -euo pipefail をセットで使うことが推奨されます。

Shell
#!/bin/bash
set -euo pipefail

# -e: エラー時に終了
# -u: 未定義変数参照時にエラー
# -o pipefail: パイプ途中のエラーも検知

パイプラインでの終了ステータス

パイプ | を使った場合、通常の $?「最後に実行されたコマンド」 の結果しか返しません。

Shell
# command1が失敗しても、command2が成功すれば $? は 0 になる
command1 | command2
echo $?

この問題を解決するには、Bash固有の配列変数 ${PIPESTATUS[@]} を使用します。

これにより、パイプ内の各コマンドの終了ステータスを個別に取得できます。

まとめ

Bashで前のコマンドが失敗した時の処理を適切に行うことは、自動化スクリプトの信頼性を高めるために不可欠です。

本記事で紹介した手法を振り返ると、最も手軽なのは || 演算子を用いたワンライナーでの記述です。

一方で、条件に応じて複雑な分岐を行いたい場合は if 文や $? 変数の活用が適しています。

また、大規模なスクリプトでは trapset -e を活用して、システム全体でエラーを監視する設計が求められます。

エラーハンドリングは「面倒な作業」と思われがちですが、「失敗したときに安全に止める、あるいはリカバーする」 仕組みを組み込んでおくことで、結果的に運用コストの削減とトラブルの早期解決に繋がります。

ぜひ、日々のスクリプト作成に取り入れてみてください。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!