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

2021-04-22Bash

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

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

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

trap コマンドの引数

trap コマンドの主な引数は以下の 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 という文字列が出力されています。

無事トラップされました。

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 )と送信されたシグナル( SIGTERM )が一致しなかったためです。

$ ./test.sh
Terminated

すべての終了シグナルをトラップしなければならないの?

人生の終わり方が様々であるように、シェルスクリプトの終了の仕方も様々です。
シグナルのうち、スクリプトを終了させるものを 終了シグナル と呼びます。
終了シグナルを全てトラップするのは面倒です。

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 シグナルをトラップして後処理を仕掛けましょう。

2021-04-22Bash