深夜に流したスクリプトが途中で落ちて、一時ディレクトリだけが残っていた。
そんな後始末の取りこぼしを防ぐのが、シェル組み込みの trap コマンドです。
私も cron で回すスクリプトを書くたびに、終了処理をどこへ置くか毎回迷っていました。
今回は GNU bash 5.1.16 で実際に動かしながら、trap の使いどころと引っかかりやすい落とし穴を整理します。
trap は「あとで実行する処理」を予約するコマンド
trap は、シグナルや特定のイベントを受け取ったときに実行するコマンドを、あらかじめ登録しておくシェル組み込みコマンドです。
書式は trap 実行するコマンド イベント名 という3つの並びをとります。
イベント名には SIGINT や SIGTERM といったシグナルのほか、bash 独自の EXIT と ERR も指定できました。
いま何が登録されているかは、trap -p で確認できます。
$ bash -c 'trap "echo hi" EXIT; trap -p EXIT'
trap -- 'echo hi' EXIT
hi
EXIT を捕まえれば後片付けは1行で済む
いちばん出番が多いのは EXIT です。
スクリプトが正常に終わっても、途中で異常終了しても、必ず一度だけ発火してくれます。
作業用の一時ディレクトリを消す処理は、この EXIT にまとめてしまうのが定石。
#!/bin/bash
set -euo pipefail
tmp=$(mktemp -d)
trap 'echo "[trap] EXIT status=$?"; rm -rf "$tmp"' EXIT
echo "start"
false
echo "never reached"
4行目より下のどこで落ちても、一時ディレクトリは必ず消えます。
手元で流すと、false で止まったあとにきちんと後片付けが走りました。
$ bash cleanup.sh
start
[trap] EXIT status=1
$ echo $?
1
注目したいのは、trap の中で参照した $? に 元の終了ステータス 1 がそのまま残っていること。
「なぜ落ちたか」をログへ書き出す用途にも、そのまま流用できるわけです。
引用符の違いで挙動が変わるのが最大の落とし穴
trap でいちばん事故が起きやすいのは、登録するコマンドの囲み方。
二重引用符で囲むと、trap を書いたその行で変数が展開されてしまいます。
単一引用符なら、発火した瞬間の値で展開される。
同じ処理に見えて、結果はきれいに割れました。
# 二重引用符: 定義した時点の値で固定される
tmp=/tmp/first
trap "echo double-quote: $tmp" EXIT
tmp=/tmp/second
# => double-quote: /tmp/first
# 単一引用符: 発火した時点の値になる
tmp=/tmp/first
trap 'echo single-quote: $tmp' EXIT
tmp=/tmp/second
# => single-quote: /tmp/second
mktemp のように あとから値が入る変数 を消したいとき、二重引用符で書くと空文字を rm しかねません。
trap の中身は単一引用符、と覚えてしまうのが安全ですね。
ERR と TERM を足して異常系まで拾う
EXIT だけでは「どこで落ちたか」までは分かりません。
そこで ERR を足すと、失敗したコマンドの行番号を $LINENO から拾えます。
#!/bin/bash
set -euo pipefail
trap 'echo "[ERR] line $LINENO: exit $?"' ERR
echo one
grep -q nothing /etc/hostname
echo two
5行目で止まったことが、たった1行のログから読み取れました。
one
[ERR] line 5: exit 1
外から kill されるケースには TERM を登録しておきます。
trap 'echo "[trap] TERM received"; exit 143' TERM
trap 'echo "[trap] EXIT status=$?"' EXIT
この状態で kill -TERM を送ったところ、TERM の処理が走ったあとに EXIT が status=143 で発火しました。
143 は 128 に SIGTERM の 15 を足した値で、シグナル由来の終了ステータスはこの決まりに従います。
同じイベントに2回書くと後勝ちになる
意外と知られていないのが、1つのイベントに登録できる処理はひとつだけ、という制約です。
同じ EXIT へ2回書くと、あとから書いたほうで丸ごと上書きされます。
#!/bin/bash
trap 'echo A' EXIT
trap 'echo B' EXIT
# 実行結果は B だけ。A は消える
共通処理を読み込む構成だと、知らないうちに相手の trap を潰していた、という事故が起こりがち。
処理は関数にまとめ、trap からはその関数を呼ぶ 形にしておくと、あとから足しやすくなりますね。
登録を取り消したいときは、コマンドの代わりにハイフンを渡すだけ。
trap - EXIT # EXIT の登録を解除
trap 'echo x' INT TERM # 1回の指定で複数シグナルへ登録
まとめて指定したあとに trap -p を叩くと、SIGINT と SIGTERM の2行が返ってきました。
cron やバックグラウンド実行と組み合わせるなら、このあたりもあわせてどうぞ。
最後に
trap は、書くのは1行なのに、事故の後始末をまるごと引き受けてくれるコマンドでした。
まず EXIT で後片付け、次に ERR で原因の記録、この2つを足すだけでスクリプトの信頼度はぐっと上がります。
そして忘れてはいけないのが、中身を単一引用符で囲むこと。
次に一時ファイルを作るスクリプトを書くとき、1行だけ足してみてください。
以上です。











コメントを残す