【Shell Script】set -eなどエラーハンドリングの基本:シェルスクリプトを安全に止める書き方

Shell Script

こんにちは、かつコーチです。

sed・awkでのテキスト処理に続いて、今回はエラーハンドリングを扱います。

前提知識ありきで、set -eを中心に、実務で必ずと言っていいほど踏む落とし穴まで踏み込んで解説します。

エラーハンドリングとは:前提の整理

シェルスクリプトは、デフォルトではあるコマンドが失敗しても、そのまま次の行に処理が進んでしまいます

車に例えるなら、シェルスクリプトのデフォルト動作は「アクセルを踏み続けたら、障害物にぶつかってもそのまま走り続ける」状態に近いです。

set -eは、この挙動を変えて「コマンドが失敗したら、その場でスクリプトを止める」という自動ブレーキの役割を果たすオプションです。

バックアップスクリプトやデプロイスクリプトのように「途中で失敗したら、それ以上進んではいけない」処理では、このブレーキが安全装置として不可欠になります。

set -eの基本

set -eの効果と書き方

set -eは、スクリプトの先頭で宣言するだけで有効になります。

#!/bin/bash
set -e

echo "処理1: 開始"
false  # 必ず失敗するコマンド
echo "処理2: ここは実行されない"

set -eがない場合、falseが失敗してもecho "処理2"は実行されてしまいますが、set -eがあるとfalseの時点でスクリプトが終了し、「処理2」は表示されません。

長いデプロイスクリプトの途中で1つのコマンドが失敗したのに、後続の処理がそのまま走ってシステムを中途半端な状態にしてしまう、という事故を防げます。

set -eが効かない代表的なケース

set -eには、初見だと見落としやすい例外がいくつかあります。

代表的なのが、コマンドがif文の条件式や&&||の一部として使われている場合です。

#!/bin/bash
set -e

if false; then
    echo "ここには来ない"
fi
echo "if文の条件式内の失敗ではスクリプトは止まらない"

false && echo "この行は実行されない"
echo "&&の左辺の失敗でもスクリプトは止まらない"

これは仕様であり、バグではありません。

if文は「条件が成立するかどうか」を判定するのが目的なので、条件式部分のコマンドが失敗しても、それは想定内の動きとして扱われます。

set -eを付けたのに、一部のエラーだけスクリプトが止まらない」と感じたときは、そのコマンドが条件式や論理演算子の一部になっていないかをまず疑ってください。

関連オプションとtrapの活用

set -uとset -o pipefailの役割

set -eだけでは防ぎきれない事故が2つあります。

1つ目は未定義変数の参照です。

set -uを付けると、定義されていない変数を参照した時点でエラーにできます。

#!/bin/bash
set -u

echo "$UNDEFINED_VAR"

これを実行すると、次のようなエラーになります。

./script.sh: line 3: UNDEFINED_VAR: unbound variable

2つ目はパイプの途中のコマンドが失敗しても、set -eだけでは検知できないという問題です。

パイプでつないだコマンドの終了ステータスは、デフォルトでは「一番最後のコマンドの終了ステータス」だけが採用されます。

set -o pipefailを付けると、パイプの中のどれか1つでも失敗したら、パイプ全体を失敗として扱えるようになります。

#!/bin/bash
set -euo pipefail

grep "ERROR" not_exist.log | wc -l

set -o pipefailがない場合、grepが「ファイルが見つからない」エラーで失敗しても、後続のwc -lが正常終了する(0件と出力される)ため、パイプ全体としては成功したように見えてしまいます。

set -o pipefailを付けておくことで、この「途中の失敗が握りつぶされる」事故を防げます。

trapでクリーンアップ処理を仕込む

trapは、スクリプトが終了するタイミング(正常終了・異常終了を問わず)で特定の処理を実行させる仕組みです。

非常口の案内図に近いイメージで、「何が起きても、退出時には必ずこの処理を通す」という保険をかけられます。

#!/bin/bash
set -euo pipefail

TMP_DIR=$(mktemp -d)

cleanup() {
    echo "一時ディレクトリを削除します: $TMP_DIR"
    rm -rf "$TMP_DIR"
}
trap cleanup EXIT

echo "作業用ディレクトリ: $TMP_DIR"
# ここで何らかの処理を行い、途中で失敗しても
# trapで登録したcleanupが必ず実行される

trap cleanup EXITと書いておくと、スクリプトが正常終了してもset -eによって異常終了しても、cleanup関数が必ず呼ばれます。

一時ファイルの削除やロックファイルの解放など、「後片付け」が必要な処理では定番の書き方です。

よくあるつまずきポイント・エラー対処

パイプの失敗が握りつぶされて気づかなかった実体験

以前、ログを集計してSlackに通知するスクリプトをset -eだけで運用していたことがあります。

#!/bin/bash
set -e

curl -s https://example.com/api/logs | jq '.errors | length' > error_count.txt

ある日、APIサーバー側の一時的な障害でcurlが失敗していたのですが、スクリプトはエラーを吐かずに正常終了し、error_count.txtには空の内容が書き込まれていました。

原因は、パイプの最後のコマンドであるjqが「入力が空でもエラーにはならず、正常終了扱いになっていた」ためです。

set -eだけではこのパターンを検知できず、curlの失敗に半日ほど気づけませんでした。

✅ After:set -o pipefailを追加してパイプ全体の失敗を検知する

#!/bin/bash
set -euo pipefail

curl -s https://example.com/api/logs | jq '.errors | length' > error_count.txt

set -o pipefailを追加したことで、curlが失敗した時点でパイプ全体がエラー扱いになり、スクリプトがその場で止まるようになりました。

外部コマンドやAPIをパイプでつなぐ処理を書くときは、set -euo pipefailを最初からセットで書く習慣をつけておくことを強くおすすめします。

応用・一歩先の使い方

set -euo pipefailをテンプレート化する

実務でシェルスクリプトを書くときは、次のようなヘッダーをテンプレートとして最初に貼り付けておくと安全です。

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

trap 'echo "エラー発生: ${LINENO}行目でスクリプトが失敗しました" >&2' ERR

trapERRシグナルを指定すると、set -eによってスクリプトが終了する直前に、失敗した行番号($LINENO)を出力させることもできます。

次回のcronによる定期実行の記事、さらに次のバックアップスクリプトの実践編でも、このテンプレートをベースに組み立てていきます。

まとめ

この記事のポイント

  • set -eはコマンド失敗時にスクリプトを止める「自動ブレーキ」で、if文の条件式や&&の左辺では効かない
  • set -uは未定義変数の参照を検知し、set -o pipefailはパイプ途中の失敗を検知する
  • trapを使うと、正常終了・異常終了を問わず後片付け処理を必ず実行できる
  • 実務ではset -euo pipefailをヘッダーとしてテンプレート化しておくと事故を防げる

次に読むべき記事

  • cronで定期実行するスクリプトを組む
  • 実践:ファイルを自動でバックアップするスクリプトを作る

タグ: Shell, 上級者向け, 実践Tips

タイトルとURLをコピーしました