シェルスクリプトでシグナルをトラップするには、みたいなことを考えることがあって、そういえば trap という日頃なかなか思い出されない不憫なビルトインがあることを思い出しました。trap は bash のビルトインとして存在する便利なヤツですが、忘れられがちなので、ここで一つ、trap コマンドとはどういうものかを書いてみることにしました。
trap の基本
trap は、その名前の通り、シグナルを補足(trap) するためのシグナルハンドラを設定するためのビルトインです。 シェルスクリプトを長時間動かすみたいなユースケースは結構あって、別言語で実装されたバッチプログラムの起動だったり、ファイルツリーを探索して何か集計したりするとか、他システムとファイルを送受信したりです。 こういう長時間動かすケースではときどき、「あれ、意図せずシグナルを送られちゃったりしたときどうしよう」みたいなことを考えなければならないケースがあります。
- この処理をしてるときにシグナルを受け取ってしまったら、変な一時ファイルが置かれたままになっちゃう
- シグナルが発生したときは、ログだけは出して終了しなきゃ
みたいなかんじ。
実際に trap を使ってみる
まぁ良くある使用例としてはこんなかんじです。 起動すると無限ループになりますが、Ctrl-C で SIGINT を発行すると、"exiting..." という文字列を出力した後に停止します。
trap 'echo exiting...; exit' SIGINT count=0 while :; do sleep 1 count=$((count+1)) echo $count done
trap 使い方
trap は次のように、シグナルを受信したときに実行するコマンド列を第一引数に、その対象のシグナルを第ニ引数以降に指定します。
trap 'commands to be executed' signals
シグナルを無視したい (デフォルトのシグナルハンドラを動かさない) ときは、コマンドを空文字列 ''
で指定すれば良いですし、デフォルトのシグナルハンドラに復元したいときは、-
を指定することになります。
例えば、先程のスクリプトの trap
の行を trap '' SIGINT
に書き換えると、Ctrl-C でも停止しないスクリプトのできあがりです。
それでまぁ、例としては上記のように echo
を使った例が使用されるわけですが、いくらなんだってシグナルハンドラが 1 行で書けることなんざ、そうそうありません。
次のように ;
で区切ってやればいくつでもコマンド列を指定することは可能ですが、クソみたいに可読性が悪くなります。
trap 'echo hello; echo world; exit' SIGINT
実は trap
の第一引数には関数名が記述できるので、次のようにすることで可読性が高まります。
function echo_hello() { echo "hello" echo "world" exit } trap echo_hello SIGINT
疑似シグナル
というように、trap ではシグナルハンドラを指定できるわけですが、ここで指定できるシグナルというのは SIG
から始まる実シグナルだけではありません。
疑似シグナルという、シグナルのようであってシグナルではないものを指定することができ、これによって便利な使い方ができるようになります。
疑似シグナルとしては、以下のようなものがあります。
ERR
EXIT
DEBUG
ERR
ERR は、コマンドの実行結果が非零のステータスであった場合に送出される疑似シグナルです。 こちらも百聞は一見にしかずなので、実行例を見てみましょう。 次のコードでは、エラーが発生したときに「どのファイルの」「どの行で」「どのコマンドが」エラーとなったのかを標準出力に出力するようにしました。
#!/bin/bash trap 'echo "[$BASH_SOURCE:$LINENO] - "$BASH_COMMAND" returns not zero status"' ERR echo "hello world" ls /not-exist-file
上記のスクリプトを動かすと、次のような実行結果になり、ls
がエラーになったことが分かります。
このようなデバッグ情報の埋め込みや、共通的なエラー処理に使用すると便利です。
hello world ls: cannot access '/not-exist-file': No such file or directory [./trap-err.sh:5] - ls /not-exist-file returns not zero status
EXIT
EXIT 疑似シグナルは、スクリプトが終了した時点で送出される(と考える)疑似シグナルです。 一時ファイルを片付けるときなどに使ったりしますね。
#!/bin/bash function cleanup() { rm -f /cleanup-target echo "cleaned up!" } trap cleanup EXIT
DEBUG
DEBUG
疑似シグナルは、コマンド毎に送出される(と考える)疑似シグナルで、名前の通り、デバッグ時に使用することが多いです。
下記のような行をスクリプトに仕込むと、一行一行の実行前にユーザ入力を要求させることができ、シェルが何を実行しようとしているのかを確認しながら実行を進めることができたりします。
trap '(read -p "[$BASH_SOURCE:$LINENO] $BASH_COMMAND?")' DEBUG
$ ./trap-test.sh [./trap-test.sh:11] trap echo_hello SIGINT? [./trap-test.sh:13] count=0? [./trap-test.sh:14] :? [./trap-test.sh:15] sleep 1? [./trap-test.sh:16] count=$((count+1))? [./trap-test.sh:17] echo $count? 1 [./trap-test.sh:14] :? [./trap-test.sh:15] sleep 1? [./trap-test.sh:16] count=$((count+1))? [./trap-test.sh:17] echo $count? 2
そんなこんなで、bash のことは嫌いでも、trap のことは嫌いにならないでください。