trapを使ったシェルスクリプトの終了処理のテンプレ（POSIX準拠）

# はじめに

trapを使ったシェルスクリプトの終了処理を確実に行うテンプレコードの紹介です。POSIX 準拠で書いています。「POSIX 準拠」の意味は、POSIX 標準規格の内容から読み取れる各シェルの微妙な動作の違いに対応することです。えぇ、面倒でしたよ。ちゃんと読めば書いてありますが、ちゃんと読むのは大変です。なお、終了処理とはシェルスクリプトの実行中に作成した一時ファイルの片付けなどのことを指しています。

:::note warn
訂正: 記事公開時にトラップアクションの冒頭でシグナルの終了ステータスが取得できると書いていましたが、CTRL+Cを除いて取得できませんでした。記事の関連する内容を書き換え、正しく動作しない「テンプレコード（簡略版）」を削除し、作り直した「テンプレコード (exit版・POSIX非準拠）の追加」を追加しました。
:::

## テンプレコード（killシグナル再送信版）

まず、面倒な話が嫌な人のために、テンプレコードです。コピーして（修正して）適当に使ってください。

- 2024-12-27 コードを短くしました
- 2024-12-28 コードをさらに短くしました

```sh
# 終了処理（$1: シグナル名）
cleanup() {
  # 終了処理中に無視するシグナル（他に必要な場合は追加する）
  trap '' HUP INT QUIT PIPE TERM

  # ここに終了処理を書く
  : TODO 

  # 自分自身にシグナルを再送信することでシェルスクリプトを終了する
  trap - EXIT "$1"
  [ "$1" = EXIT ] || kill -s "$1" $$ || exit 1
}

# トラップするシグナル（他に必要な場合は追加する）
for i in EXIT HUP INT QUIT PIPE TERM; do
  trap 'cleanup '"$i" "$i"
done

# ここより下に実際のシェルスクリプトの内容を書く
    :
    :
```

自分自身にシグナルを再送信しているところが特徴的だと思いますが、その理由についての解説がこの記事の内容で、POSIX 準拠で作るにはこうしなければいけなかった部分です。この記事の後半には、完全な POSIX 準拠ではない `exit` を使用した実装も掲載しています。解説を読んだうえでお好みでどうぞ。

## 解説

POSIX に準拠させることの何がめんどくさいかと言うと、POSIX には「シェルによって動作が違う」と書いてあるけれども実際のシェルの動作は書いていないことです。ただでさえ標準規格書は専門的なもので初学者なんかが読むのは大変なものだというのに、標準規格書を読み込んで、実際のシェルでテストして、動作の違いに対応したコードを書くのは簡単ではありません。あるシェルでは動くけれども、他のシェルでは動かない場合はいくつもあります。そのようなシェルの動作の違いに詳しくなってもプログラミング力は向上しません。ただのシェルに詳しいだけの人です。こんなものはなくして普通に書けば動くようにしたいですね。そうしなければシェルスクリプトはいつまでも他の言語よりも移植性が低いと言われ続けます。終了処理についてはこの記事で解決できたのではないかと考えています。

### trapに長いコードを書くのはやめよう

これは必須ではありませんが、次のような `trap` コマンドの中に直接埋め込むコードは読みづらいです。これは実質的に `eval` と同じなのである種の危険性もあります。

```sh
trap 'rm ...; rm ...' INT
```

単純ではない処理を行う場合には次のようにシェル関数にして、`trap` コマンドではシェル関数を呼び出すだけにしましょう。

```sh
func() {
  rm ...
  rm ...
}

trap 'func' INT
```

### シグナル番号は使うべきではない

**POSIX ではシグナル番号は、オプションの XSI 拡張機能**です。POSIX で規定されているように見えて、実際には移植性が保証されていない部分です。SUS (Single UNIX Specification) に準拠している macOS などでは、XSI 拡張機能に準拠しているはずですが、そうではない大半の Linux や BSD 系 Unix では XSI 拡張機能に準拠している保証はありません。まあ、実際には XSI 拡張機能として定義されているシグナル番号については準拠しているようですが、POSIX 準拠にこだわるのであればシグナル番号は使うべきではありません。

[POSIX trap](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#trap) には次のようにあります。[XSI]のマークとその範囲に注目してください。「XSI に準拠したシステムではこれらのシグナル番号が移植性がある」と書かれています。XSI はオプションの拡張機能であり、POSIX に準拠するだけなら実装する必要はなく、System V 系 Unix の仕様が元となっており、BSD 系 Unix では未実装または異なる意味で実装されているなどの理由で、オプション機能として扱われています。また XSI に準拠しているシステムでもこれだけの番号しか標準化されおらず、名前で使えるシグナル（[参照](https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/signal.h.html)）よりも少ない数しかありません。

> [XSI] [Option Start] XSI-conformant systems also allow numeric signal numbers for the conditions corresponding to the following signal names:

| シグナル番号 | シグナル名 |
| - | - |
| 1 | SIGHUP | 
| 2 | SIGINT | 
| 3 | SIGQUIT | 
| 6 | SIGABRT | 
| 9 | SIGKILL | 
| 14 | SIGALRM | 
| 15 | SIGTERM| 

繰り返しますが、このシグナル番号はオプションの XSI 拡張機能です。たとえ POSIX に準拠した OS だとしても（実際 Linux や BSD 系 Unix は POSIX に準拠していると主張していないわけですが）、**このシグナル番号はどの環境でも使えると保証されているわけではありません**。POSIX 標準規格書をチラ見して、なんか表が乗ってるから使っていいんだろうと考えるのは間違いです。

シグナル名を使っているところは、`kill` コマンドと `trap` コマンドです。`kill` コマンドの話は後回しにします。`trap` コマンドの使い方は以下のとおりです。

```sh:POSIX 準拠で移植性が高いとされる書き方
trap '' HUP INT QUIT PIPE TERM
```

ときたま見かけるコードには、次のように番号で指定されていますが、これが XSI 拡張機能の書き方です。POSIX 準拠を名乗るのであれば（擬似シグナル `EXIT` に対応する `0` を除いて）シグナル番号で指定してはいけません。

```sh:XSI 拡張機能なので移植性が高いとは言えない書き方
trap '' 1 2 3 13 15
```

まあ、POSIX うんぬんよりもまず、マジックナンバーでは読みづらいですよね？ System V 系 Unix での移植性のために標準化する必要があったと言うだけで、シグナル番号による指定は良い指定方法ではありません。POSIX で標準化された内容とは優れた方法でもベストプラクティスのことでもなく、アプリケーションを移植するのに必要な内容です。

### trapの処理をデフォルトに戻す方法

シグナル番号を使うべきではないと言ったばかりですが、シグナル番号に移植性がないと言うだけで、使ってはいけないわけではなく、シグナル番号を使った仕様も規定されています。シグナルの処理をデフォルトに戻すときは、普通はトラップアクションとして `-` を指定して、後ろにシグナル名を書きます。

```sh:普通はシグナル名で書く
trap - シグナル名...

例: trap - INT
```

ただし**最初の引数が符号なし10進数の場合は**シグナル番号と解釈され `-` を省略できます。と POSIX に書かれています。

```sh:一応標準化されているが書かない方がよい
trap シグナル番号...

例: trap 2
```

だから `trap 2` や `trap 2 3` などは書いてよいですが、**`trap INT` は書いたらダメ**なんです。なぜダメかと言うと、`trap INT QUIT` では、QUIT シグナルが送信された時に `INT` コマンドを実行するという意味になるからです。シェルによっては `trap INT` （シグナル名が1個だけのとき）で INT シグナルの処理をデフォルトに戻せますが、シェルによっては戻せず、POSIX をちゃんと読めば標準化されていない書式であることがわかります。

### シグナル名は大文字でSIGなし

たまに

```sh:POSIX で規定されていない
trap '' SIGINT
```

や

```sh:POSIX で規定されていない
trap '' int
```

のようなものを見かけますが、POSIX では**拡張機能としてシェルは実装してもよい**と明記されていますが、POSIX で標準化されているのは SIG なしの大文字だけです。

なぜ POSIX は SIG なしの大文字しか標準化しなかったのかといえば、もともと（POSIX 以前の）Bourne シェルの実装が、SIG なしの大文字にしか対応していなかったからです。Bourne シェルの後継のシェルの一部は Bourne シェルと同じ仕様で実装しました。その状況下で POSIX が標準規格を作ったわけですが、移植性がない SIG プレフィックスありのシグナル名の実装を必須にして、シェル実装者に修正の負担を与える意味はありませんよね？ SIG なしの大文字だけ使えれば実用上問題ありません。POSIX はそのことを文書化したのだから、POSIX をちゃんと読んだ人なら移植性がある書き方を知ることができます。

### シグナル終了時の終了ステータスは128より大きいとしか決まっていない

POSIX ではシグナルで終了した時の終了ステータスは、128 より大きいとしか決まっておらず、どのように表現すれば良いかは規定されていません。通常の方法で終了したプロセスの終了ステータスの最大値は 255 です。そして、**多くのシェルでは**シグナルで終了したときの終了ステータスは「シグナル番号 + 128」で、終了ステータスの最大も 255 なのですが、**シェルによっては**シグナルで終了したときに 256 以上の終了ステータスを扱うこともあります。
 
[POSIX exit](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#exit) にも書いてありますが、`exit` で指定できる最大の値は 255 ではなく、`$?` で取得できる値も 255 が最大ではありません。また [2.8.2 Exit Status for Commands](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#tag_19_08_02) には次のようにあります（太字部分参照）。

> The exit status of a command shall be determined as follows:
> 
> - If the command is not found, the exit status shall be 127.
> - Otherwise, if the command name is found, but it is not an executable utility, the exit status shall be 126.
> - **Otherwise, if the command terminated due to the receipt of a signal, the shell shall assign it an exit status greater than 128. The exit status shall identify, in an implementation-defined manner, which signal terminated the command. Note that shell implementations are permitted to assign an exit status greater than 255 if a command terminates due to a signal.**
> - Otherwise, the exit status shall be the value obtained by the equivalent of the WEXITSTATUS macro applied to the status obtained by the wait() function (as defined in the System Interfaces volume of POSIX.1-2024). Note that for C programs, this value is equal to the result of performing a modulo 256 operation on the value passed to _Exit(), _exit(), or exit() or returned from main().

上記の内容は POSIX.1-2024 で改定されたものであることに注意してください。それまでの内容は曖昧でした。Stéphane Chazelas による こちら https://unix.stackexchange.com/a/99134 の回答もお読みください。

### kill -lでシグナル番号とシグナル名の対応が調べられる

`kill -l` を bash で実行すると次のような一覧が出力されます。

```console
$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX
```

一見これをパースすれば、シグナル番号とシグナル名の対応が作れるように思えるかもしれませんが、これは POSIX で標準化されている形式ではありません。実際 bash でも POSIX モードで出力すると次のような形式（実際は1行）で出力されます。

```console
$ bash -o posix -c 'kill -l'
HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT
CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH IO PWR SYS 
RTMIN RTMIN+1 RTMIN+2 RTMIN+3 RTMIN+4 RTMIN+5 RTMIN+6 RTMIN+7 RTMIN+8 RTMIN+9
RTMIN+10 RTMIN+11 RTMIN+12 RTMIN+13 RTMIN+14 RTMIN+15 RTMAX-14 RTMAX-13
RTMAX-12 RTMAX-11 RTMAX-10 RTMAX-9 RTMAX-8 RTMAX-7 RTMAX-6 RTMAX-5 RTMAX-4
RTMAX-3 RTMAX-2 RTMAX-1 RTMAX
```

見て分かる通り番号がありません。小さい順番から並んでいるようですが飛んでいる番号もあるため対応づけはできません。つまり `kill -l` は使用可能なシグナル名の一覧を知るためのものであって、シグナル番号を知るためのものではないのです。じゃあシグナル番号からシグナル名への対応は作れないじゃないかと思うのは早合点で、`kill -l シグナル番号` または `kill -l シグナルの終了ステータス` を使います。この機能は POSIX で標準化れているものです。

```console
$ kill -l 2
INT

$ kill -l 130
INT
```

逆にシグナル名からシグナル番号への変換は、一部のシェルで実装されていますが、実装されていないシェルもあり POSIX でも標準化されていません。とはいえ、たかだか 127 個しか候補はないわけで 1 から 127 までループで繰り返せばシグナル名からシグナル番号への変換も可能です。今回の例では使用していませんが、作ったので公開します。

```sh
signal_list() {
  i=0
  while [ "$i" -lt 127 ] && i=$((i + 1)); do
    kill -l "$i" 2>/dev/null && echo "$i"
  done | while read -r n && read -r i; do
    echo "$n:$i"
  done
}
SIGNAL_LIST=$(signal_list)
signame2num() {
  for signal in $SIGNAL_LIST; do
    case $signal in
      $1:*) echo "${signal#*:}"
    esac
  done
}
```

### kill -15や-TERMは使うべきではない

これもよく見かけますが、POSIX では XSI 拡張機能なので移植性は保証されていません。[POSIX kill](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/kill.html) についてはこちらを参照してください。

```
SYNOPSIS

kill [-s signal_name] pid...
kill -l [exit_status]

[XSI] [Option Start]
kill [-signal_name] pid...
kill [-signal_number] pid...
[Option End]
```

SYNOPSIS にしっかり書いてあるように、`-シグナル名` や `-シグナル番号` は XSI 拡張機能なんです。まあ実際には使えるのですが、POSIX 準拠と言うからには使ってはなりません。ということで POSIX に準拠した書き方は次の書き方だけなんです。シグナル番号はありません。

```sh
kill -s TERM プロセスID...
# -s TERM は省略可能
```

`kill -9 pid` とか見かけますが、`kill -s KILL pid` と書きましょう。マジックナンバーは分かりづらいです。

ちなみに、Bourne シェルビルトインの `kill` コマンドでは `-s` オプションが使えなかったりしますが、これは Bourne シェルが POSIX 準拠ではないからです。Bourne シェルは、前世代 OS の Solaris 10 の `/bin/sh` ぐらいでしか使われていないので切り捨てましょう。

### シグナル終了時に EXIT は呼ばれないかもしれない

bash だけを対象にしていれば、`EXIT` のトラップアクションはシェルスクリプトの終了時に確実に呼ばれます。しかし、すべてのシェルがそのような動きをするわけではありません。

つまり、**`EXIT` はシグナルで終了したときには呼び出されない**（かもしれない）んです。

これもちゃんと POSIX に書かれています。というか「その事がちゃんと書いてないぞ！」ってツッコミ（[参照](https://www.austingroupbugs.net/view.php?id=621)）が入って、POSIX.1-2024 で追記されました。POSIX の仕様では、「`EXIT` のトラップアクションはシェルが普通に正常に終了（`exit` コマンドを含む）した時に呼び出される」が「シグナルで終了したときにも呼び出される**かもしれない (may occur)**」と書いてあります。

ここで POSIX 慣れしてない人は、なんで「仕様をかっちり決めないんだよ！仕様なのに曖昧にするな！」と憤慨するところですが、違うんです。だって仕様を変更してしまったら互換性が保てないでしょ？ そうしたら今までのシェルスクリプトが壊れてしまうじゃないですか？ だからシェルの実装は今まで通りで変更する必要はなく、シェルスクリプトを書く人（つまり私）が POSIX 準拠する作業をやれよと言われたわけです。つまり両対応するのが POSIX に準拠するということなんです。まあ別に POSIX に言われる前から `EXIT` の挙動が違うことは実際のシェルでテストしていて先に気づいていたので、私にとっては驚きはありませんでしたが。

シグナルでシェルスクリプトが終了した時に `EXIT` が呼び出されるのは、ksh と bash と mksh です。おそらく ksh88 が最初に実装したのを bash が真似たのでしょう。`EXIT` が呼び出されないのは Bourne シェルの仕様で、そっちを真似した ash 系のシェルではシグナルで終了した時に `EXIT` は呼び出されません。

### シグナル発生時の終了ステータスはわからない

シグナルを契機にトラップアクションが呼び出された時、そのシグナルに対応する終了ステータスはわかりません。この方法でわかるんじゃないの？って思う人がいるかも知れません。

```sh
trap_action() {
  echo $? # CTRL+C や CTRL+\ を押した時以外は 0
}
trap trap_action INT TERM
env sleep 10
# env を使用しているのは ksh93 では sleep が
# シェルビルインコマンドで挙動が異なるため
```

この方法でシグナルの終了ステータスが得られるのは **CTRL+C (INT) や CTRL+\ (QUIT) を押して終了した時だけ**です。シグナルの送信でトラップアクションが呼び出されたときの `$?` の値は 0 です。POSIX にはシグナルのトラップアクションが呼び出された時に、`$?` がシグナルの終了ステータスであることは記載されていませんし、もちろん（CTRL+C や CTRL+\ を押した時を除いて）終了ステータスはわかりません。わかる CTRL+C や CTRL+\ であっても使用するシェルによって違いがあります。

| シェル | CTRL+C | CTRL+\ |
| -- | -- | -- |
| dash、bash、mksh、ksh88 (Solaris 10) | 130 (128 + 2) | 131 (128 + 3) |
| FreeBSD sh、NetBSD sh、OpenBSD sh | 130 (128 + 2) | 131 (128 + 3) |
| ksh93 (macOS、Solaris 11)、ksh93u+m | 258 (128*2 + 2)| 259 (128*2 + 3)|
| yash | 386 (128*3 + 2) | 387 (128*3 + 3) |

ちなみに、ksh93 や yash の終了ステータスは 255 を超えていますが、POSIX 準拠としては問題ありません。シグナルで終了したときに終了ステータスが 256 以上であるのは許可された動作です（参照: [POSIX exit](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#exit)）。ただし、この内容は POSIX.1-2024 で改定された内容であり、[以前の exit](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#exit) では未定義 (undefined) でした。

> If n is specified and has a value between 0 and 255 inclusive, the wait status of the shell or subshell shall indicate that it exited with exit status n. If n is specified and has a value greater than 256 that corresponds to an exit status the shell assigns to commands terminated by a valid signal (see 2.8.2 Exit Status for Commands), the wait status of the shell or subshell shall indicate that it was terminated by that signal. No other actions associated with the signal, such as execution of trap actions or creation of a core image, shall be performed by the shell.
> 
> If n is specified and is not an unsigned decimal integer, or has a value of 256, or has a value greater than 256 but not corresponding to an exit status the shell assigns to commands terminated by a valid signal, the wait status of the shell or subshell is unspecified.

### 自分自身に kill して終了する

まず大前提の話をすると、理想的にはシグナルを受け取って終了処理を行って終了したとき、元のシグナルの終了ステータスでなければなりません。例えば PIPE シグナル（一般的には 141）でシェルスクリプトが終了したとき、それを別の終了ステータスに置き換えてしまったら、シェルスクリプトが PIPE シグナルで終了したことを検出できません。シグナルの本来の終了ステータスで終了するために、自分自身に `kill` コマンドで元のシグナルを送信して終了しています。以下の部分です。

```sh
  # 自分自身にシグナルを再送信することでシェルスクリプトを終了する
  trap - EXIT "$1"
  [ "$1" = EXIT ] || kill -s "$1" $$ || exit 1
```

このようなコードにしている理由はシグナルが呼び出された時のシグナルの終了ステータスがわからないからです。コードを見ればわかるように、シグナルが呼び出されたときのシグナル名はわかります。でもそこから終了ステータスへの変換ができません。逆はできるんですよ。終了ステータスからシグナル名への変換は。これは `kill -l 終了ステータス` をやれば OK です。POSIX で標準化されているとおりです。一応シグナル番号からのシグナル名への変換もできるので、ループで繰り返せばシグナル名からシグナル番号への変換テーブル作れますが、それではそのシグナル番号に対応するシグナルで終了したときの終了ステータスはなに？って話です。

[POSIX exit](https://pubs.opengroup.org/onlinepubs/9799919799/utilities/V3_chap02.html#exit) に書いてあることを読めば、256 以上のシグナルに対応する終了ステータスで終了すれば良いようではありますが、シグナル番号から終了ステータスへ変換する計算式はありません（シェル依存のはず）。**bashの場合**は、シグナルで終了した時に 128+N を使うとドキュメント化されています（[参照](https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html)）。

したがってより確実な方法として、自分自身に同じシグナル名でシグナルを送信することにしました。コードも短いですしね。もし `kill` が失敗した場合はフォールバックとして終了ステータス1で終了しています。自分自身への `kill` 実行が失敗するとしたら、別の問題があるはずなので終了ステータス1でも問題ないでしょう。

### zsh のシェル関数でのtrap EXITの仕様

zsh ではシェル関数の中で `trap ... EXIT` を行うと、シェル関数を終了した時にトラップアクションが呼びだされます。

```sh:zshtrap.sh
#!/usr/bin/env zsh

init_trap() {
  trap cleanup EXIT INT
  trap # trap状況の出力
}

cleanup() {
  echo "cleanup"
}

init_trap

echo "==="

trap # trap状況の出力
echo before sleep
sleep 3
```

```console
$ zsh ./zshtrap.sh
trap -- cleanup EXIT
trap -- cleanup INT
cleanup                  ← ここで cleanup 処理が呼び出されている！？
===
trap -- cleanup INT      ← 「trap -- cleanup EXIT」が消えている！？
before sleep
                         ← 最後には cleanup 処理が呼び出されない！？
```

なんでこんな仕様なのかよくわかりませんが、仕方ないのでシェル関数の外で `trap` コマンドを実行しています。ちなみに `setopt POSIX_TRAPS` を実行すると POSIX 準拠の動作に変更できるのでこの方法でも OK です。こういう POSIX に準拠していないシェルの動作があるから、POSIX だけを読んでいても移植性があるシェルスクリプトは書けないんですよね......

## テンプレコード (exit版・POSIX非準拠）

シェルスクリプトの終了ステータスを、シグナルを受け取ったときの終了ステータスで正しく終了するには、自分自身にシグナルを再送信するしかありません。しかし自分自身に `kill` するというのは気持ち悪さを感じる人（私自身）もいるでしょう。そのような人のために終了ステータスを「シグナル番号 + 128」固定で終了するコードの実装です。ksh93 や yash にとっては厳密には理想的とは言えませんが、どちらもシェルも「kill -l シグナル番号+128」からシグナル名に変換できますし、現実的な問題は発生しない（発生しないようにできる）でしょう。

```sh
# シグナルのためのトラップアクション
#   $1: 終了関数名, $2: シグナル番号
trap_action() {
  # 終了ステータスは「シグナル番号 + 128」固定とする
  set -- "$1" $(($2 + 128))
  "$1"

  # 終了処理関数が再度呼び出されるシェルのために
  # exitするだけの関数に再定義する
  eval "$1() { exit $2; }"
  exit "$2"
}

# 終了処理
cleanup() {
  # 終了処理中に無視するシグナル（他に必要な場合は追加する）
  trap '' HUP INT QUIT PIPE TERM

  # ここに終了処理を書く
  : TODO 
}

i=0
while [ "$i" -lt 127 ] && i=$((i + 1)); do
  n=$(kill -l "$i" 2>/dev/null) || :
  # トラップするシグナル（他に必要な場合は追加する）
  case $n in (HUP | INT | QUIT | PIPE | TERM)
    trap 'trap_action cleanup '"$i" "$n"
  esac
done
trap 'cleanup' EXIT

# ここより下に実際のシェルスクリプトの内容を書く
    :
    :
```

冒頭のコードよりもコード量が増え、少々技巧的なテクニックを使っていますが、これはグローバル変数の使用を避け（初期化部分でのグローバル変数の使用は、最初に使うだけで後のことは考えなくていいため問題ありません）、修正のしやすさを考慮して上で妥協したコードです。もっと短くしようと思えばできますが、ここらが妥当だろうという判断です。古い ksh93 ではシェル関数の中で自分自身を再定義すると、まれにシェルのバグで落ちることがあるという現実的な問題も考慮しています。

## それでも・・・

### シェル毎に動作の違いはある

このコードを使えば、どの POSIX シェルでも動きますが、違いがないわけではありません。それはトラップしていないシグナルを受け取ったときです。テンプレコードにはシェルスクリプトから処理したいであろうシグナルを書いていますが、もしこれ以外のシグナルを受け取ったとき、bash、ksh、mksh では `EXIT` が呼び出されるので終了処理が行われますが、それ以外のシェルでは終了処理が行われません。したがってその他のシグナルに対応する場合は、そのシグナルをトラップする必要があります。

### 終了処理が行われないことはある

`KILL` シグナルや `STOP` シグナルはトラップできません。それに加えてシステムを強制的に終了させた場合は、終了処理なんてできるわけがないので、終了処理は正しく行われないことがあるという前提で作る必要があります。例えば次回起動時に、終了処理が行われていなければ復旧処理（不要なファイルなどの削除）を行うとかですね。 一時ファイルの作成であれば `/tmp` ディレクトリを利用していれば、そんなに気にすることではないかもしれませんが。

## さいごに

ということで、解説でごちゃごちゃ面倒だった話を書いていますが、最終的にはテンプレコードのようなシンプルで理解可能な形に持っていけたと思います。まあ、短時間で複数のシグナルが送信されたとき、処理が割り込まれるスキがあるような気もしているのですが、シェルの実装がどうなっているかに依存しますし、検証した限りでは問題なかったのでこのままにしています。あと古いシェルとかでは十分テストしていないので、問題が発覚したらこっそり修正します。

おまけで POSIX があっても各シェルの違いはなくならない大変さをわかっていただければと思います。「POSIX に準拠する」というのは、こういうことを考えながらプログラムすることなんです。シェルの数だけ動作に違いがあります。対象シェルは一つ（bash が現実的）に限定するのが簡単です。私は必要があるので多数の POSIX シェルを相手にしていますが、つくづく POSIX 準拠のシェルスクリプトは素人にはオススメできない世界だなと思います。