スクリプト終了イベントの捕捉と zsh の非互換性 - 拡張 POSIX シェルスクリプト Advent Calendar 2013 - ダメ出し Blog

2013-12-05(Thu) [sh][shell][zsh] [更新履歴]

拡張 POSIX シェルスクリプト Advent Calendar 2013、5日目の記事です。 書いているうちにネタが変わってしまいました。

trap というシェル組込みコマンドをご存じですか? シグナルを受信したときに実行する処理 (シグナルハンドラー) を設定するものです。

例えば SIGINT シグナル※を受信するとデフォルトではスクリプトが終了しますが、 trap で捕まえてやると終了せずに継続することも可能です。 (※割り込み (INTerrupt)。 端末から実行したシェルスクリプトなら Ctrl+c で送信可)

$ sh -c 'trap "echo Boo!" INT; while :; do read i && echo "$i"; done'
…Ctrl+cを押しまくる…
^CBoo!
^CBoo!
^CBoo!
…Ctrl+zを押す…
zsh: suspended  sh -c 'trap "echo Boo!" INT; while :; do :; done'
$ jobs
[1]  + suspended  sh -c 'trap "echo Boo!" INT; while :; do :; done'
$ kill %1
[1]  + terminated  sh -c 'trap "echo Boo!" INT; while :; do :; done'

この例では Ctrl+c では終わらせることができないので、 Ctrl+z でサスペンドしてから殺すなどしてください。


ところで手元の ksh 93u+ (2012-08-01) で試すとサスペンドできないんですが、 なんでだろう。read 中が駄目っぽい。どうしてなのか誰か調べて教えてください!!

$ ksh -c 'trap "echo Boo!" INT; while :; do read i && echo "$i"; done'
…Ctrl+cを押しまくる…
Boo!
Boo!
Boo!
…Ctrl+zを押してもサスペンドせず無視される…

ksh -c read の実行をサスペンドできるか各種 ksh で軽く調べたところ、 以下は大丈夫。

以下は駄目。新し目の AT&T ksh に問題があるっぽい。

AT&T 由来の ksh は、このところ積極的に保守されているようだし、 もし気が向いたら調査してバグ報告してみたいところ。


閑話休題。

trap は、シグナル受信だけでなく、 いくつかのイベント発生時のハンドラーを設定することもできます。 以下のようなものです。

ああ、ようやく本題に入れます。今回紹介したいのはこの中の EXIT です。 これを trap でひっかけることで、スクリプトの終了時に任意の処理を実行できます。 C 言語を知っている人であれば atexit(3) 相当と思っていただけるとわかるかと。

$ sh -c 'trap "echo Bye!" EXIT; echo "Hello!"'
Hello!
Bye!

簡単ですね。

SIGINTSIGTERM シグナルを受けたときどうなるでしょうか。

$ sh -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'
Bye!
zsh: terminated  sh -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'
$ bash -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'
Bye!
zsh: terminated  bash -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'
$ ksh -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'
Bye!
zsh: terminated  ksh -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'
$ zsh -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'
zsh: terminated  zsh -c 'trap "echo Bye!" EXIT; kill -TERM $$; echo "Huh?"'

zsh: terminated 〜 は対話シェルに利用している zsh が出力しているメッセージ

でました、zsh の非互換! zsh はシグナルハンドラー※内で終了すると、 EXIT ハンドラーを実行してくれません。酷い。 (※この例では SIGINT のデフォルトのシグナルハンドラー)

ほかの sh と同じ動作を実現できないか、もう少し工夫してみます。

$ zsh -c 'trap "echo Bye!" EXIT; trap "exit -1" TERM; kill -TERM $$; echo "Huh?"'
zsh: exit 1     zsh -c 'trap "echo Bye!" EXIT; trap "exit 1" TERM; kill -TERM $$; echo "Huh?"

駄目でした。 ※ zsh: exit 1 〜 は対話シェルに利用している zsh が出力しているメッセージ

では、これでどうだ!

$ zsh -c 'atexit(){ echo "Bye!"; }; trap atexit EXIT TERM; kill -TERM $$'
Bye!
Bye!

!? (AA略) 意味がわかりません。バグっぽい臭いがプンプンします。 もう嫌になってきました。

$ zsh -c 'atexit(){ echo "Bye!"; }; trap atexit EXIT TERM; kill -TERM $$'
Bye!

それっぽい動きになりました。しかし、よーく考えるとわかるのですが、 これだとどのイベントを契機に終了したのかスクリプトの終了コードで判別できなくなってしまいます。

$ zsh -c 'atexit(){ echo "Bye!"; }; trap atexit EXIT; trap "atexit; exit -1" TERM; kill -TERM $$'
Bye!
zsh: exit 255   zsh -c

ようやくほかの sh に近い動きになりました。

このスクリプトが bash でどうなるか試してみましょう。

$ bash -c 'atexit(){ echo "Bye!"; }; trap atexit EXIT; trap "atexit; exit -1" TERM; kill -TERM $$'
Bye!
Bye!
zsh: exit 255   bash -c

アハハハ八八八ノ ヽノ ヽノ ヽ (AA略) まだだ! まだ終わらんよ!!

$ bash -c 'atexit(){ echo "Bye!"; }; trap atexit EXIT; trap "trap - EXIT; atexit; exit -1" TERM; kill -TERM $$'
Bye!
zsh: exit 255   bash -c
$ zsh -c 'atexit(){ echo "Bye!"; }; trap atexit EXIT; trap "trap - EXIT; atexit; exit -1" TERM; kill -TERM $$'
Bye!
zsh: exit 255   zsh -c

ふぅ…。

こんな感じで bash, ksh, zsh には微妙な動作の違いがあったりするので、 ちょっと変わったことしようとするときは特に注意しましょう。


ところで、12月25日はクリスマスな上に、 OSS 界隈で地味に活躍されているふみやすさんの誕生日ですね!
http://www.amazon.co.jp/registry/wishlist/27M7TV8CEEF6G?sort=priority

逆に、あなたの書いた OSS や Blog や Advent Calendar が気に入ったら何か送りたく なってしまうかもしれないので、プロフィールや Web サイトに あなたの Amazon 欲しいものリストの URL を貼っておいてくださいね!


私が勤める OSSTech っていう某弊社で社員募集しているようです。 人材紹介会社を介さなければ、入社後に 20万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


よろしければ、これまで参加した/参加予定のほかの Advent Calendar もどうぞ。