スクリプト終了イベントの捕捉と 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万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → http://www.osstech.co.jp/company/recruit

バツイチでアラフォーでほとんど諦めていますが、新たなパートナーも募集中です。 ちゃんと愛情表現してくれて精神的に自立している女性がいいです。お友達から始めましょう。 → https://twitter.com/satoh_fumiyasu


よろしければ、過去の Advent Calendar もどうぞ。