【シェルスクリプト】Ctrl-C で実行中のジョブを突然殺すことなく、ジョブの終了をちゃんと待ちたい
動画のエンコードのような「時間のかかる処理」をループ内で繰り返し実行するようなシェルスクリプトを書いた場合、
Ctrl-C でいきなり終了するのではなく、現在実行中の「時間のかかる処理」が終わってから終了するようにしたい、て場合があるかと思います。
trap
コマンドで SIGINT (Ctrl-C) をトラップしてゴニョゴニョすればできそうですが、それだけだと「時間のかかる処理」は Ctrl-C を受け取って終了してしまいます。
じゃあどうするかというと、こう。
#!/usr/bin/env sh # なにやら時間のかかる処理を定義 heavy_task () { sleep 3 } # SIGINT (Ctrl-C) を受け取ったら interrupted 変数に値を代入 trap "echo "Ctrl-C をトラップ"; interrupted=1" INT # ループ開始 while true; do echo "処理開始…" # なにやら時間のかかる処理を「バックグラウンドで」実行 heavy_task & # バックグラウンドで起動したコマンドの終了を wait $! で待機。 # なおかつ、wait コマンドがシグナルにより終了させられたらすかさず再wait while wait $!; [ $? -ge 128 ]; do : done # 時間のかかる処理が終了 echo "終了!" # interrupted 変数に値が代入されてたらループを抜ける if [ -n "$interrupted" ]; then break fi done echo "bye!"
この中でポイントはここでしょうか。ちょっとブレークダウンして説明します。
while wait $!; [ $? -ge 128 ]; do : done
$!
は直前に起動したバックグラウンドプロセスのPID。wait $!
とすることで、バックグラウンドジョブとして起動した heavy_task
が終了するのを待機することになります。
待機中、もしユーザーが Ctrl-C をタイプすると wait
がそれを受け取って終了してしまうのですが、その時の終了コードが 128 + SIGINTの番号 になります。これはシェルの仕様です。
$ man bash … The return value of a simple command is its exit status, or 128+n if the command is terminated by signal n. ざっくり訳:コマンドがシグナル n により終了した場合、終了コードは 128 + n になる …
wait
の終了コード $?
が 128 より大きいかチェックすれば ([ $? -ge 128 ]
) 、シグナルによる終了なのか、それとも単に heavy_task
が無事完了した結果 wait
が終了したのか、区別できるわけです。
シグナルによる終了だった場合は即 wait
し直しているので、heavy_task
が終わるまで待ち続けられます。
同時に、Ctrl-C を trap
コマンドで検知して interrupted
変数に値を代入し、その内容をループの最後でチェックすることで、ループを抜けるべきか続けるべきか判断しています。
応用編:Ctrl-C 2回で強制終了
さらに拡張して、2回 Ctrl-C したら有無を言わさず強制終了するようにしたのがこちら。
#!/usr/bin/env sh heavy_task () { sleep 3 } # SIGINT (Ctrl-C) を受け取ったら1回目は interrupted 変数に、2回目以降は force_finish 変数に値を代入 function sigint_handler () { if [ -z "$interrupted" ]; then echo "実行中の処理の完了後に終了します(もう1度 ^C で強制終了)" interrupted=1 else echo "強制終了!" force_finish=1 fi } trap sigint_handler INT while true; do echo "処理開始…" heavy_task & while wait $!; [ $? -ge 128 ]; do # trap により force_finish 変数に値が代入されていたら「強制終了」の指示とみなす if [ -n "$force_finish" ]; then # 実行中の heavy_task を kill if ps $! >/dev/null; then kill $! fi break fi done echo "終了!" if [ -n "$interrupted" ]; then break fi done echo "bye!"
便利!