trap を使ってシェルスクリプトの後処理を楽に制御する

シェルスクリプトは、その可搬性からビルドスクリプトやセットアップスクリプトに利用されることが多い。

今回は、後処理やシグナル処理使われる trap というコマンドを紹介する。

trap コマンドは、指定したシグナルを受信したときに実行するコマンドを設定できる。先に例を見せる。

```long_sleep.sh
#!/bin/bash

function trap_sigint() {
    echo "割込を検出"
}

trap trap_sigint sigint # 第2引数のsigint は int とも書ける

echo "長考"
sleep 100 # 長い処理
echo "終了"
```

long_sleep.sh を実行している最中に `Ctrl+C` を入力し SIGINT を送ると次のようになる。

```
$ bash long_sleep.sh
長考
^C割込を検出
終了
```

上記のコードには注意するべき点がある。それは `割込を検出` と表示されたあと `終了` と表示されている。つまり、 trap は実行中のコマンドに割り込んでいるだけで、後続の処理は実行される。

検証してみよう。

```long_double_sleep.sh
#!/bin/bash

function trap_sigint() {
    echo "割込を検出"
}

trap trap_sigint sigint # 第2引数のsigint は int とも書ける

echo "長考"
sleep 10 # 長い処理1
sleep 10 # 長い処理2
echo "終了"
```

このスクリプトは、2度 SIGINT を送らないと `終了` と表示されない。

```console
$ bash long_double_sleep.sh
長考
^C割込を検出
^C割込を検出
終了
```

これを踏まえると、なんらかのシグナルを受け取ったとき後続処理を中止するためには trap で呼び出すスクリプトの中で終了させなければならない。

```exit_when_interrupt.sh
#!/bin/bash

function trap_sigint() {
    echo "割込を検出"
    exit 0 # exit status 0 は正常終了
}

trap trap_sigint sigint # 第2引数のsigint は int とも書ける

echo "長考"
sleep 10 # 長い処理1
echo "後続処理"
sleep 10 # 長い処理2
echo "終了"
```

上記のように書けば後続処理は実行されず、 `終了` とも表示されない。
処理を確実に中止できる。

```console
$ bash src/trap.bash
長考
^C割込を検出
```

# 実践

例として、「一時ファイルを作成し、途中で SIGINT を受け取ったら一時ファイルを削除するスクリプト」を作る。

一時ファイルを作るときは、 mktemp を使うこと。詳しくは、 `man mktemp` を参照。

```postprocess.sh
#!/bin/bash

set -e

function postprocess () {
    echo "作ったファイルを削除"
    rm -f $TMPFILE
}

trap signal_int int

TMPFILE=`mktemp`

# see: http://tldp.org/LDP/abs/html/exitcodes.html
function signal_int () {
    postprocess
    exit 130 # 128 + SIGINT
}

echo "一時ファイル" >> $TMPFILE
echo $TMPFILE
cat $TMPFILE
sleep 10 # 重い処理
echo "最後まで終了"
```

途中で `Ctrl` + `C` で SIGINT を送ってもファイルは削除される

```console
$ bash postprocess.sh                                         
/var/folders/0x/thnv3ql10dd6ksxsyy1wb6hh0000gn/T/tmp.GnpgZZi2
一時ファイル
^C作ったファイルを削除

$ # ファイルが存在しないことを確認
$ ls -al /var/folders/0x/thnv3ql10dd6ksxsyy1wb6hh0000gn/T/tmp.GnpgZZi2
ls: /var/folders/0x/thnv3ql10dd6ksxsyy1wb6hh0000gn/T/tmp.GnpgZZi2: No such file or directory
```

## Exit Status について

http://tldp.org/LDP/abs/html/exitcodes.html によれば、 bash の exit status (あるいは exit code とも呼ばれる) は一部予約されている。 

正常終了は 0、一般的なエラーは 1、 致命的なエラーシグナルは シグナル値 + 128 で予約されている。

さきほどの後処理をするスクリプトでは、 SIGINT のシグナル値は 2 であるため、 130 という exit status を設定した。

ほかのシグナル値が気になる場合は、 `man signal` で調べると良い。

## 正常終了したときも一時ファイルを削除する

上記のスクリプトでは、割り込まれなかったときファイルが消されない問題があるのでさらに修正する。

```postprocess2.sh
#!/bin/bash

set -e

function postprocess () {
    echo "作ったファイルを削除"
    rm -f $TMPFILE
}

trap signal_int int
trap postprocess EXIT # 正常終了したときに後処理を実行

TMPFILE=`mktemp`

# see: http://tldp.org/LDP/abs/html/exitcodes.html
function signal_int () {
    postprocess
    exit 130 # 128 + SIGINT
}

echo "一時ファイル" >> $TMPFILE
echo $TMPFILE
cat $TMPFILE
sleep 10 # 重い処理
echo "最後まで終了"
```

```console
$ bash postprocess2.sh
/var/folders/0x/thnv3ql10dd6ksxsyy1wb6hh0000gn/T/tmp.r1e9Pr4G
一時ファイル
最後まで終了
作ったファイルを削除

$ # ファイルが削除されていることを確認
$ ls -al /var/folders/0x/thnv3ql10dd6ksxsyy1wb6hh0000gn/T/tmp.r1e9Pr4G
ls: /var/folders/0x/thnv3ql10dd6ksxsyy1wb6hh0000gn/T/tmp.r1e9Pr4G: No such file or directory
```

重要な箇所は、 trap を使って exit というシグナルがきたときに postprocess を呼び出しているところである。 この exit は疑似シグナルと呼ばれるものの一つで、正常終了直前に送られる。詳細は `man bash` の trap の節を読むといい。疑似シグナルには、 EXIT のほかに DEBUG 、 ERR がある。