Bashシェルスクリプトで終了前に後始末をさせたい時はtrapコマンドを活用しよう

2022-04-10Bash

Bash シェルスクリプトの終了時に、 必ず後処理をさせたい 場合があります。
例えばスクリプト内で作成した 一時ファイルを削除する といった場合です。

そんな時に使えるコマンドとして、Bash ビルトインコマンドである trap コマンドがあります。

trap コマンドを使ってスクリプト終了前に後処理をさせてみたいと思います。

trap コマンドの引数

trap コマンドの主な引数は以下の 2 つです。

  1. 実行させたいシェルスクリプトコマンド
  2. トラップを仕掛けたい シグナル

突然出てきましたがここで言う シグナル とは何でしょう?

シグナルとは?

シグナルとは、 プロセスとプロセスの間で通信を行う際に使用される信号 のことです。
ターミナル上のインタラクティブな操作の場合、2 つのプロセスは以下のようになります。

  • 一方 は実行中プロセス
  • 他方 はユーザーにより実行される kill コマンドだったり、ショートカットキー操作

実際の例をみていきましょう。

trap コマンドでシグナルをトラップする

まずは以下のような簡単なスクリプトを作成します。

test.sh

#!/usr/bin/env bash

trap 'echo "trap SIGINT"' SIGINT

cat

trap コマンドで SIGINT ( 後述 ) というシグナルをトラップさせるようにしておきます。
cat コマンドは単体で利用された場合、ユーザーの入力を永遠に待ち続けます。

作成したスクリプトを実行します

# スクリプト実行
$ ./test.sh

catコマンドのおかげで、スクリプトが実行中のまま終了しません。

{ctrl} + {c} キーを押して、 SIGINT シグナルを送信する

実行中のスクリプトに対して SIGINT シグナルを送信してみます。

SIGINT シグナルは、実行中のプロセスに対して {ctrl} + {c} キーを押すことで送信できます。試してみます。

$ ./test.sh
^Ctrap SIGINT

通常、 {ctrl} + {c} キーを押すと実行中のスクリプトが中断されるだけですが、今回は trap SIGINT という文字列が出力されています。

SIGINTシグナルがトラップされたためです。

kill コマンドで SIGINT シグナル以外 を送信する

今度は SIGINT シグナル以外 のシグナルを送信してみます。

もう一度同じスクリプトを実行した後、別ターミナルに開き、実行中のプロセスを一覧表示します。

# 実行中プロセス一覧
$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  11840  2896 pts/0    Ss   11:09   0:00 /bin/bash
root        18  0.0  0.0  11840  2888 pts/1    Ss   11:09   0:00 bash
root       112  0.0  0.0  11700  2484 pts/0    S+   12:47   0:00 bash ./test.sh
root       113  0.0  0.0   4396   744 pts/0    S+   12:47   0:00 cat
root       115  0.0  0.0  51744  3352 pts/1    R+   12:47   0:00 ps aux

スクリプト実行プロセス ( bash ./test.sh ) が見つかります。
このプロセスに対して kill コマンドで シグナル を送信してみます。
kill指定されたプロセス に対して SIGTERM シグナル を送信するコマンドです。

$ kill 112

スクリプトは終了しますが trap コマンドにしかけていたコマンドが実行されません。
これは、 trap コマンドに仕掛けていたシグナル( SIGINT )と kill コマンドによって送信されたシグナル( SIGTERM )が一致しなかったためです。

$ ./test.sh
Terminated

「終了」を判定するにはいくつものシグナルをトラップしなければならないの?

人生の終わり方が様々(大往生、事故、病気、...)であるように、シェルスクリプトの終了の仕方も様々(SIGINT、SIGTERM、...)です。
シグナルのうち、スクリプトを終了させるものを 終了シグナル と呼びます。

終了シグナル がいくつもあるということは、ひとくくりに 「終了」 と呼んでいるものをトラップするためには、 全ての 終了シグナル をトラップする必要があるのでしょうか?

これは面倒ですね。

EXIT シグナルをトラップする

すべての 終了シグナル 発生をトラップする良い方法があります。

終了シグナルが実行された後には EXIT というシグナルが送信されます。
少し変わったシグナルで、 終了シグナルが送信された後に 自分自身に対して送信するという特性があります。

  1. 別プロセスが SIGINT シグナルを送信
  2. SIGINT シグナルを受信
  3. 自プロセスに EXIT シグナルを送信
  4. EXIT シグナルを受信

先ほどのスクリプトを書き換えて、trap するシグナル名を EXIT に変えてみます。

#!/usr/bin/env bash

trap 'echo "trap EXIT"' EXIT

cat

変更後、スクリプトを実行します。
別のターミナルで SIGINT シグナルと SIGTERM シグナルを送信してみます。

# スクリプト実行後に CTRL+C を押して、 SIGINT シグナルを送信
$ ./test.sh
^Ctrap EXIT
# スクリプト実行後に kill <PID> を実行して、SIGTERM シグナルを送信
$ ./test.sh
trap EXIT
Terminated

今度は SIGINT シグナル、 SIGTERM シグナルのいずれが送信された場合でも、 trap の処理が実行されていることがわかります。

問答無用で強制停止させる SIGKILL シグナル

終了シグナルの中でも SIGKILL というシグナルはさらに変わった特性があります。
どうしようもなかった時に、プロセスを強制終了するためのシグナルになりますが、できるだけ使わない方がよいです。
このシグナルは EXIT トラップを使っても捕まえることができないためです。

プログラム内で想定されている後始末処理が実行されずにプロセスが頓死してしまうので、いろいろな弊害が起きる可能性があります。不要なプロセスを止めたいと思ってもできるだけ SIGKILL プロセスは送信しないようにしましょう。

# EXITシグナルをトラップするスクリプトを実行
$ ./test.sh
# プロセスを一覧表示
$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  11840  2896 pts/0    Ss   11:09   0:00 /bin/bash
root        18  0.0  0.0  11840  2888 pts/1    Ss   11:09   0:00 bash
root       113  0.0  0.0   4396   744 pts/0    T    12:47   0:00 cat
root       120  0.0  0.0   4396   760 pts/0    T    12:59   0:00 cat
root       122  0.0  0.0  11700  2620 pts/0    S+   13:04   0:00 bash ./test.sh
root       123  0.0  0.0   4396   760 pts/0    S+   13:04   0:00 cat
root       124  0.0  0.0  51744  3372 pts/1    R+   13:04   0:00 ps aux

# killコマンドで 122プロセスに対して KILL シグナルを送信。 ( -KILL でKILLシグナルを送信できる )
$ kill -KILL 122

trap により後処理が実行されません。

$ ./test.sh
Killed

ひとこと

ややこしいことを忘れて、とりあえず EXIT シグナルをトラップして後処理を仕掛けましょう。

2022-04-10Bash