理系学生日記

おまえはいつまで学生気分なのか

シェルスクリプトでのtrapの使い方

シェルスクリプトでシグナルをトラップするには、みたいなことを考えることがあって、そういえば 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 から始まる実シグナルだけではありません。 疑似シグナルという、シグナルのようであってシグナルではないものを指定することができ、これによって便利な使い方ができるようになります。

疑似シグナルとしては、以下のようなものがあります。

  1. ERR
  2. EXIT
  3. 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 のことは嫌いにならないでください。

参考文献

  1. Using traps in your scripts – IBM Developer
  2. How "Exit Traps" Can Make Your Bash Scripts Way More Robust And Reliable