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

2023-03-31Bash

はじめに

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

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

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

trap コマンドの使い方

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

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

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

シグナルとは?

シグナルとは、 プロセスとプロセスの間で通信を行う際に使用される信号 のことです。

ターミナル上で実行中のコマンドを {ctrl} + {C} で停止させた、といった場合を例に上げましょう。
この場合も「一方のプロセス」に対して「他方のプロセス」がシグナルを送った結果、実行中のコマンドが停止されます。

  • 一方 は実行中のコマンドプロセス
  • 他方 はユーザーにより実行されるショートカットキー操作 ( {ctrl} + {C})

このように、シグナルを受け取ったプロセスの状態は、通常の処理実行から変化します。

文面だけではわかりにくいため、 trap コマンドを使った具体的な例をみながらシグナルについても理解していきましょう。

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

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

test.sh

#!/usr/bin/env bash

trap 'echo "trap SIGINT"' SIGINT

cat

trap コマンドは 2 つの引数を受け取っています。
第 2 引数で指定した SIGINT ( 後述 ) というシグナルをトラップします。
トラップに成功すると、第 1 引数のコマンドを実行します。

trap コマンドを実行したとしても、処理の流れはすぐ次の行に進みます。

cat コマンドは単体で利用された場合、ユーザーの入力を永遠に待ち続けます。
結果、このスクリプトは最終行の cat 実行タイミングから先に進みません。

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

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

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

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

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

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

$ ./test.sh
^Ctrap SIGINT

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

{ctrl} + {c} キーが押されたタイミングで ./test.sh スクリプトにシグナルが送信された結果 SIGINT シグナルがトラップされtrap の第 1 引数のコマンドが実行されたことがわかります。

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 コマンドにしかけていたコマンドが実行されません。
これは、 ./test.sh スクリプト内で trap コマンドに仕掛けていたシグナル ( SIGINT )と kill コマンドによって送信されたシグナル ( SIGTERM ) が一致しなかったためです。

$ ./test.sh
Terminated

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

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

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

終了シグナルの文だけ、 trap コマンドを呼び出す処理を記述するのは大変です。

良い方法はないでしょうか?

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 シグナルを受けて停止した場合にはメモリ内のデータが消えてしまいます。

不要なプロセスを止めたいと思ってもできるだけ 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 シグナルをトラップして後処理を仕掛けましょう。

2023-03-31Bash