良いシェルスクリプトのためのkillとtrapの基本 ～ シグナル番号は使わない、シグナル名を使う

## はじめに

`kill` コマンドと `trap` コマンドはシェルでシグナルの送信と受信を行うためのコマンドです。このコマンドは意外と適切ではない使われ方をよく見かけます。この記事では `kill` と `trap` の基礎知識を解説します。

関連記事: こちらに `trap` を利用した終了処理のコードを書きました。
[trapを使ったシェルスクリプトの終了処理のテンプレ（POSIX準拠）](https://qiita.com/ko1nksm/items/1606e5fadb734e8c5b3f)
 
## POSIX準拠のkillコマンドの構文

[POSIX で標準化されている kill コマンド](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/kill.html)の使い方は次のとおりです。POSIX で標準化されているというのは移植性が高い書き方ということを意味しています。シグナル名の指定には `-s` オプションが必要です。そして `signal_name` であって `signal_number` でないことに注意してください。**POSIX シェルの世界にシグナル番号という概念はありません**。シグナル番号を指定した書き方は避ける方をおすすめします。

```txt:POSIXで標準化（必須）された書き方
kill [-s signal_name] pid...
kill -l [exit_status]

例: kill 1234, kill -s TERM 1234, kill -l 141
```

**POSIX では SIG 接頭詞を付けたシグナル名に対応することは要求されていません**。dash ではエラーになります。

```console:killで SIG 接頭詞は付けない
$ dash -c 'kill -s SIGTERM $$'
dash: 1: kill: invalid signal number or name: SIGTERM

$ dash -c 'kill -s TERM $$'
Terminated
```

次のような使い方はよく見かけます。一応どの環境でも動くはずです。**POSIX で標準化されているように見えますがオプションの XSI 拡張**となっているので実装されない環境があっても不思議ではありません。

```txt:XSI拡張（オプション機能なので実装されない環境があっても不思議ではない）
kill [-signal_name] pid...
kill [-signal_number] pid...

例: kill -9 1234, kill -TERM 1234
```

この構文は一言で言うならば **POSIX で標準化される前の古い書き方**です。元々廃止予定の書き方だったものが POSIX.1-2001 での Single UNIX Specification の POSIX への統合で XSI 拡張として生き残ってしまったものです。XSI 拡張とは簡単に言えば System V 系 Unix 用の仕様で、BSD Unix との移植性を考慮していません。

XSI 拡張の構文は POSIX のオプションのガイドラインに準拠していないため POSIX では対応を要求していないのでしょう。**いろんな書き方を覚える必要はありません。最善の書き方である最初の書き方を使用してください**。癖がついてしまっている人を強制する意図はありませんが、これからの人に使わせる必要はありません。シンプル is ベストです。説明するならこのような使い方もできるという補足に留めておきましょう。

シグナル番号の使用に関する一つの例外は `0` です。`0` はプロセスが生きているか（シグナルが送信可能か）を調べるための「シグナル名」として使うことができます。

```console:プロセスが生きているか（シグナルが送信可能か）を調べる
$ kill -s 0 # 0 はシグナル番号のようだがシグナル名扱い
```

## trapでシグナル番号やSIG接頭詞は使わない

まず避けるべき書き方を提示します。

```sh:避けるべき書き方
trap 'echo int' 2       # シグナル番号は POSIX で必須となっていない
trap 'echo int' SIGINT  # 頭に SIG を付けると動かないシェルがある
trap 'echo int' int     # 小文字には対応していないシェルがある
```

理由はコメントに書いた通りですが、これらは POSIX で標準化されている書き方ではありません。**POSIX としては `SIGINT` が `2` であるという保証はしていません**（XSI拡張を実装している環境を除く）。以下の表は XSI 拡張としての定義です。シグナル番号とシグナル名の対応は POSIX でこのように実装すべきと必須にしてないため、（おそらくこの通りになっているはずですが）もしかしたら違うかもしれません。

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#trap

> [XSI] 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 で標準化された範囲で使うことができます。

https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/signal.h.html

```txt
SIGABRT SIGALRM SIGBUS  SIGCHLD SIGCONT SIGFPE  SIGHUP  SIGILL
SIGINT  SIGKILL SIGPIPE SIGQUIT SIGSEGV SIGSTOP SIGTERM SIGTSTP
SIGTTIN SIGTTOU SIGUSR1 SIGUSR2 SIGWINCH SIGURG

[XSI拡張]
SIGSYS SIGTRAP SIGVTALRM SIGXCPU SIGXFSZ
```

また `trap` コマンドで冒頭に `SIG` 接頭詞を付けたり小文字で書くのは推奨しません。一部のシェルでエラーになります。

```console
$ dash -c 'trap "echo int" SIGINT'
trap: SIGINT: bad trap

$ zsh -c 'trap "echo int" int'
zsh:trap:1: undefined signal: int
```

移植性なんか気にしないという考え方もあると思いますが、頭に `SIG` を付けるのは単純に長くて入力が面倒な上に可読性の向上などのメリットはないので短く書きましょう。

## kill -lでシグナル番号からシグナル名に変換

プロセスがシグナルで終了したときは「シグナル番号+128」の終了ステータスで終了します。

```console
$ sleep 100 & # バックグランドで sleep コマンドを実行
[1] 2488442

$ kill -s INT 2488442 # sleep コマンドに SIGINT (シグナル番号 2) を送信
$ wait $! # sleep コマンドの終了ステータスを取得
[1]+  Interrupt               sleep 100

$ echo $? # sleep コマンドの終了ステータス（シグナル番号2 + 128）
130
```

この `130` というシグナル番号をシェルスクリプトに記述してはいけません。なぜなら SIGINT のシグナル番号は `2` とは限らないからです。

この場合、`kill -l` を使用すれば終了ステータスからシグナル名を取得することができます。**この使い方は POSIX で標準化された使い方**です。

```console:POSIX で標準化された使い方
$ kill -l 130 # 終了ステータスからシグナル名に変換可能
INT

$ kill -l 2 # シグナル番号からもシグナル名に変換可能
INT
```

一部の実装ではシグナル名からシグナル番号に逆変換をすることができますが、これは POSIX で標準化されておらず、実際に動作しないシェルがあります。

```console:POSIX で標準化されていない使い方
$ bash -c 'kill -l INT' # シグナル名からシグナル番号への変換はできるとは限らない
2

$ dash -c 'kill -l INT'
dash: 1: kill: Illegal number: INT

$ mksh -c 'kill -l INT'
E: mksh: kill: bad number: INT

$ yash -c 'kill -l INT'
INT
```

## kill -l（引数なし）から対応表は作れない

`kill -l` （引数なし）からシグナル番号とシグナル名の対応表を作ることはできません。これはシェルによってはシグナル番号が出力されないからです。

```console
$ bash -c '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

$ bash --posix -c 'kill -l' # bash は POSIX モードではシグナル番号を出力しない
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

$ dash -c 'kill -l'
0
HUP
INT
QUIT
 ︙
RTMAX

$ ksh -c 'kill -l'
HUP
INT
QUIT
 ︙
RTMAX

$ zsh -c 'kill -l'
HUP INT QUIT ILL TRAP IOT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS
```

`kill -l` の出力形式は POSIX で次のように標準化されています。

```
"%s%c", <signal_name>, <separator>
```

この時 `separator` は改行またはスペースです。したがって bash も（POSIX 準拠モードでは）POSIX に準拠した出力ですが、シグナル番号は出力されません。`kill -l` の出力形式はシェルによってかなり異なります。すべてのシェルに対応すれば対応表を作ることは不可能ではないかもしれませんが、終了ステータスは 0 から 255 までしか無いわけで、もし対応表が必要であれば、ループして `kill -l 終了ステータス` から対応表を生成したほうが良いでしょう。もっとも必要になることはあまりないと思います。

## trap -l は使わない（bash専用）

bash では `kill -l` と同じ意味で `trap -l` が使えますが、わざわざ使う必要はありません。POSIX でも標準化されていません。

## さいごに

ということで、良いシェルスクリプトのための `kill` と `trap` の使い方でした。実践的な使い方としては説明することはまだまだあるのですが、長くなるのでまずはよく見かける、良くない使い方の訂正です。