それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

bashの強制終了(CTRL+C)検出と後処理/trap

bash でコマンドを実行して強制終了するとき

ターミナルでコマンドを実行して応答がないときに強制終了したい。こようなときは次のコマンドを使います。

Ctrl + C  強制終了

ときどき見かける。どこかで間違った知識が流布している。Ctrl+z を押す人が多い

Ctrl + z 一時停止

一時停止の方がレスポンスが速く、間違ったコマンドを即座に出力停止できることと、やり直す(Undo)のキーなので混同されやすいのだと思われます。 ctrl + z でジョブ(プロセス)を一時停止した場合は再度実行に戻すことが出来ます。 fg / bg コマンドがそれに当たります。 fg / bg はまた別の記事に書きますので参考にしてください。

強制終了を検出したい

Ctrl +C で強制終了したコマンドは、ただ強制終了されると困ることが有ります。

作業中ファイルどうするの。

強制終了されてこまること。それが中間ファイルの後始末を出来ないということです。

そもそも中間ファイルを作らないようにプログラムを作ればいいのですが。気をつけても作ってしまうことが有ります。 後始末を出来たほうがいいこともあります。ネットワーク接続やトランザクションやファイルロック、ジョブキューなど

作業中のファイルを消すためにtrapする

trap することで強制終了後に後処理を済ませることが出来る。

trap の使い方

trap の使い方は、ほぼこの定型文です。

trap コマンド 1 2 3 15 

trap を使ってみる。

さっそく、TRAPを使ってみたいと思います。無限ループするコマンドを用意し、無限ループを強制終了したら、なにか文字列を出力することにしてましょう。

無限ループコマンドtrapつき:例
function last () {
  status=$?
  echo '強制終了しました'
  echo "ステータス: $status"
  echo  in trap, status captured
  exit $status
}

trap 'last'  {1,2,3,15}

i=0
while : ; do
  echo $(( i++ ))
  sleep 1
done
無限ループを強制終了した例
takuya@Desktop$ bash trap.sh
0
1
^C
強制終了しました
ステータス: 130
trap captured

ほかにも kill -HUP PID などで実行中のbash無限ループを止めてみると色々変化を見ることが出来ます。

ただし、 kill -KILL PID でKILLした時は、本当の意味の強制終了 SIGKILLが発生するので trap も発動しません。

シグナルについて

今回はあまりSIGNALには深追いをしないつもりなので、あまり記述しません。

必要箇所だけ触れておきます。

Ctrl + C で送信しているのは SIGINT です。SIGINTとは、signal interrupt キーボードからの強制割り込みによる中断です

この他にSIGHUP があます。 SIGHUPとは signal hangup つまり回線切断です。これはログイン中のユーザーが回線ダウンによりどっか行ったぞ。って意味です。

ちなみに、冒頭の強制終了をCtrl+z 勘違いの話をしましたが、Ctrl+zで放置しててもプロセスが終了するのは、 一時停止後にログアウトでSIGHUPが発生し、プロセスが終了するからです。間違って覚えていた人は、このSIGHUPがあるおかげで暗線に一時停止で誤魔化せていたわけです。

そして、 SIGKILL があります。これは純粋な強制終了で、trap も発動しません。即死技です。

*1

キーボード シグナル 数字 意味
Ctrl + C INT / SIGINT 2 interrupt 強制割り込み中断です。よく使う
HUP / SIGHUP 1 hangup 回線切断による中断
KILL / SIGKILL 9 KILL 即死技です。trapする隙きも与えず終了
Ctrl+Z STP/SIGTSTP 20 stop 一時停止です。

TRAP で終了後処理ができる

trap を使うことで、終了後の後処理が出来ます。シグナルにトラップして捕まえるのです。

trap の例1
trap 'echo hooooooooooooooooooo' 1 2 3 15 
trap の例2
function () {
  echo hoooooo
}
trap 'my_trap' 1 2 3 15 
trap の例3
function () {
  echo $@
}
trap 'my_trap hooo' {1,2,3,15}

わたしは、TRAPするシグナルを列挙するよりブレース展開で囲って書くほうがシグナルがわかりやすくて好きです。

trap を書くときの注意

trap を書くときの注意があります。

できるだけ最初に書く

trap はできるだけ最初に書くべきです。次の例では、無限ループの後ろにTRAPが来ています。trap を設定する前に、無限ループが走ってしまい、Ctrl+Cで止めてもトラップされません。

trap 出来ない トラップ
i=0
while : ; do
  echo $(( i++ ))
  sleep 1
done

trap 'echo trap'  {1,2,3,15}

trap でシグナルの上書きができてしまう。

trap をしていると異常終了をそのまま異常終了出さずに正常終了にしてしまうことが出来ます。もちろんそのための例外処理なので出来て当たり前なのですが。スクリプトを書いていると、エラーになるはずがエラーにならず困っていしまいます。そこでトラップするとき終了コードを維持しておいたほうが無難でしょう。

function last_trap () {
  status=$?
  exit $status
}

trap 'last_trap'  {1,2,3,15}

その他の trap

trap は他にも、正常終了でも毎回実行する事もできます。

正常終了のときにだけ実行されるtrapが作れる
function success () {
   echo $?
}
trap 'success' 0
function failure () {
   echo $?
}
trap 'failure' 1 2 3 15

これは、一見すると、正常終了であるため、スクリプトの本筋でやるべきです。だらべつに何も使う必要はないと思いますが、デバッグ時などに便利です。

実行前実行が出来る DEBUG

debug をつかえば、実行前に仕込むことが出来ます。

function before () {
   echo $?
}
trap 'before' DEBUG

まとめ

trap により異常終了時のファイルの後始末などが出来る。

ただし、ソースコードに jump / goto が増えるのでそんなに多用するべきではありません。

ファイルが存在したら消して新規作成などのスクリプトを本来は書くべきです。

ただし、ファイルのロックの後始末や多重起動防止や排他制御にはあると重宝します。またデバッグにも便利です。

シェルスクリプトは中間ファイルをなるべく作らないほうがいいと個人的には思います。そのためにパイプライン処理があり、 /tmp などの一時記憶もあります。が

参考資料

trap: trap [-lp] [[arg] signal_spec ...]
    シグナルまたは他のイベントをトラップします。

    シェルがシグナルを受け取るか他の条件が発生した時に実行されるハンドラーを
    定義および有効化します。

    ARG はシグナル SIGNAL_SPEC を受け取った時に読み込まれ実行されるコマンド
    です。もし ARG が無い (かつシグナル SIGNAL_SPEC が与えられた場合) または
    `-' の場合、各指定したシグナルはオリジナルの値にリセットされます。
    ARG が NULL 文字列の場合、各シグナル SIGNAL_SPEC はシェルにおよび起動さ
    れたコマンドによって無視されます。

    もし SIGNAL_SPEC が EXIT (0) の場合、ARG がシェルの終了時に実行されます。
    もし SIGNAL_SPEC が DEBUG の場合 ARG は単に毎回コマンドの前に実行されます。
    もし SIGNAL_SPEC が RETURN の場合 ARG はシェル関数または . か source に
    よって実行されたスクリプトが終了した時に実行されます。 SIGNAL_SPEC が ERR
    の場合、-e オプションが有効な場合にシェルが終了するようなコマンド失敗が発
    生するたびに実行されます。

    もし引数が与えられない場合、 trap は各シグナルに割り当てられたコマンドの
    一覧を表示します。

    オプション:
      -l    シグナル名とシグナル番号の対応一覧を表示します
      -p    各 SIGNAL_SPEC に関連づけられた trap コマンドを表示します

    各 SIGNAL_SPEC は <signal.h> にあるシグナル名かシグナル番号です。シグ
    ナル名は大文字小文字を区別しません。また SIG 接頭辞はオプションです。
    シグナルはシェルに対して "kill -signal $$" で送ることができます。

    終了ステータス:
    SIGSPEC が無効か、無効なオプションを与えられない限り成功を返します。

*1: SIGINT など 接頭詞の SIG は SIGNAL の略語で、無しで表記されることもあります。