現場で使えるシェルスクリプト入門12:堅牢なエラーハンドリング(set -e/pipefail)とtrapによる自動クリーンアップ

シェルスクリプト IT

前回の記事(入門11)では、繰り返しのもう一つの主役である「while文」と、ファイルを安全に行ごとに処理する「while read」構文の使いこなし、そしてパイプラインとサブシェルの落とし穴について解説しました。

今回は、作成したシェルスクリプトを本番環境や自動ジョブ(Cronなど)で「安全かつ堅牢に動かす」ための最重要テーマである「エラーハンドリング」「自動クリーンアップ」について解説します。

デフォルトのシェルスクリプトは「途中でエラーが起きても無視して次の行を実行する」という挙動になります。この挙動を制御し、想定外のトラブルやデータ破損を未然に防ぐ書き方をマスターしましょう!

デフォルト挙動の落とし穴:エラーを無視して突き進むスクリプト

シェルスクリプトは、基本的に記述したコマンドを上から順に実行していきます。しかし、途中のコマンドがエラー(終了ステータスが0以外)になっても、デフォルトでは処理を中断せず、そのまま次のコマンドを実行し続けます。

これがなぜ危険なのか、以下の例を見てみましょう。

#!/bin/bash
# 存在しないディレクトリに移動しようとする(エラーになる)
cd /nonexistent_directory

# ディレクトリ移動に失敗したにもかかわらず、直下のファイルを全削除してしまう
rm -rf *

もしこのスクリプトを実行すると、最初の cd コマンドが失敗したにもかかわらず処理が続行され、実行した時のカレントディレクトリ(最悪の場合、ホームディレクトリやシステム重要フォルダ)にあるすべてのファイルが強制削除されてしまいます。これがデフォルト挙動の恐ろしさです。

安全対策その1:エラー時に即停止させる「set -e」と未定義変数を防ぐ「set -u」

こうした致命的な事故を防ぐために、スクリプトの冒頭(シバン #!/bin/bash の直後)に記述すべきなのが set -eset -u です。

💡 コマンド失敗で即終了する「set -e」

スクリプト内に set -e を記述すると、「実行したコマンドのいずれかが失敗(終了ステータスが0以外)した場合、その場でスクリプトの実行を即座に終了する」という設定に切り替わります。

#!/bin/bash
set -e

cd /nonexistent_directory # ここで失敗するため、スクリプトが即終了する
rm -rf *                  # この危険なコマンドは実行されない!

これだけで、予期せぬエラーの連鎖による事故を防ぐことができます。

💡 未定義変数の参照をエラーにする「set -u」

シェルスクリプトは、定義していない変数(スペルミスなど)を参照した場合、エラーにならず「空文字」として処理を進めてしまいます。これもバグの原因になります。

#!/bin/bash
target_dir="/tmp/work"

# スペルミスで「trget_dir」と書いてしまった(空文字に展開される)
# 結果的に「rm -rf /」に等しい超危険コマンドに変貌する
rm -rf "$trget_dir/"

冒頭に set -u を指定しておくと、未定義の変数を参照した時点で「unbound variable」というエラーを出して即時停止してくれるようになります。

安全対策その2:パイプラインの落とし穴を防ぐ「set -o pipefail」

非常に強力な set -e ですが、一つだけ重大な抜け穴があります。それが「パイプライン(`|`)で繋がれたコマンドのエラーを検知できない」という問題です。

#!/bin/bash
set -e

# 存在しないファイルを表示しようとするため cat は失敗するが、
# パイプライン全体の終了ステータスは「最後のコマンド(wc -l)」のもの(成功/0)になるため、
# スクリプトは停止せず進んでしまう
cat nonexistent.txt | wc -l

echo "この行が実行されてしまう!"

実行結果:

# ./test1.sh 
cat: nonexistent.txt: No such file or directory
0
この行が実行されてしまう!

シェルスクリプトのデフォルト仕様では、パイプライン全体の終了ステータスは「一番右側のコマンドの終了ステータス」になります。そのため、途中でエラーが起きていても、最後のコマンドが成功すれば set -e をすり抜けてしまいます。

これを防ぐために、set -o pipefail を設定します。このオプションを有効にすると、「パイプライン内のいずれかのコマンドが失敗した場合、パイプライン全体の終了ステータスを失敗(非0)として扱う」ようになり、set -e で正しくスクリプトを停止させることができます。

set -euo pipefail

# この行で止まる
cat nonexistent.txt | wc -l

echo "この行が実行されてしまう!"

実行結果:

# ./test1.sh 
cat: nonexistent.txt: No such file or directory
0

安全対策その3:「trap」コマンド

set -e などでスクリプトが途中で安全に停止するようになっても、別の問題が発生します。それが「処理の途中で作成したゴミがサーバーに残ってしまう」などの問題です。

これを解決するのが、シグナルを監視して指定の処理を割り込ませる trap コマンドです。

trapコマンドは、一言で言うと「もし特定のイベントが起きたら、この処理を自動で実行する」という予約を事前に登録しておく仕組みです。Web開発における「イベントリスナー」や、何か起きた時の「防犯センサー」のようなものと考えるとイメージしやすいでしょう。

基本的な構文は非常にシンプルです。

trap '実行したい処理' イベント名

登録する代表的なイベント(トリガー)には以下のようなものがあります。

イベント名発生するタイミング
EXITスクリプトの処理が終了した時(正常終了・エラー終了どちらも含む)
INT実行中にユーザーが Ctrl + C を押して強制終了(割り込み)させた時
TERM外部のシステムや他のプロセスから終了命令(killなど)を受け取った時

💡 trap によるクリーンアップ

一時ファイル(`mktemp`)を作成し、スクリプトが正常終了した時はもちろん、エラーで落ちた時に一時ファイルを削除する実装例がこちらです。

#!/bin/bash
set -euo pipefail

# 1. 安全な一時ファイルを作成
tmpfile=$(mktemp /tmp/script_work.XXXXXX)

# 2. スクリプト終了時(EXIT)に必ず一時ファイルを削除するよう trap に登録
# 正常終了、エラー終了、Ctrl+Cによる割り込み等に対応します
trap 'rm -f "$tmpfile"; echo "一時ファイルを削除しました。"' EXIT

# --- メイン処理 ---
echo "処理を開始します..."
echo "データ" > "$tmpfile"

# 何らかのエラーが発生したと仮定(set -e によりここで即終了する)
ls /nonexistent_dir

echo "この行には到達しません。"

このスクリプトを実行すると、ls /nonexistent_dir でエラーが発生して即座に処理が中断されますが、中断された瞬間に trap に登録した rm -f "$tmpfile" が自動的にトリガーされ、サーバー内のゴミファイルを削除して終了します。

まとめ

実務の現場でシェルスクリプトを書く際は、以下の3点セットを冒頭に記述すると予期せぬエラーを防げます。

#!/bin/bash
set -euo pipefail
オプション効果防げるトラブル
-eコマンド失敗時に即時終了するエラー発生後のコマンド暴走によるファイル誤削除やデータ破損
-u未定義変数を見つけたらエラーにする変数のスペルミスによる意図しないディレクトリでのコマンド実行
-o pipefailパイプラインの途中エラーも検知するパイプラインの左側で起きたサイレントなエラーのすり抜け

これにtrap コマンドを組み合わせて記述すれば、不測の事態に自動で対処する環境を構築できます。ぜひあなたのスクリプトでも試してください!

コメント

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