dashで逐次的trap

逐次的にtrapできたらgoのdeferみたいでかっこいい気がした。
サブシェル内で使えば一時ファイルのスコープを簡単に制限できる。

```bash:イメージ
( 
  touch /tmp/tmpfile1.$$
  trap_push "rm /tmp/tmpfile1.$$" EXIT
  ...
  touch /tmp/tmpfile2.$$
  trap_push "rm /tmp/tmpfile2.$$" EXIT
  ...
) # スコープ脱出と同時にpushした一時ファイルたちが削除される
```

調べたところ[この記事](https://blog.cles.jp/item/7821)が見つかった。
概要はtrap -pで現行のtrap処理を取り出して、処理を追加して
再度trapに登録するものだが、trap -pがdashで使えなかったので、
dash版を作成しようと思った。

## trap出力の解析の難しさ
trapの出力は以下の性質を持つ。

1. 各シグナルの情報は改行区切り
1. シングルクォートによるエスケープがある
1. エスケープ内は改行はもちろんシングルクォート自身やtrap, EXIT等どんな文字列も含みうる

```:偏狭なtrapの例
$ trap "echo 'hoge'" 0
$ trap 'echo "EXIT
> trap --"' 2
$ trap
trap -- 'echo '\''hoge'\''' EXIT
trap -- 'echo "EXIT
trap --"' INT
```

上記の例だと、"^trap"や"EXIT$"では1シグナル分の情報を拾えないことが分かる。

```:誤った分析
$ trap | grep "EXIT$"
trap -- 'echo '\''hoge'\''' EXIT
trap -- 'echo "EXIT
```

この文字列は正規表現の表現力を超えるため、パースするには
(本気で一文字ずつパースするのは除いて)evalを使う必要がある。

```:evalでsetしようとする例
$ eval "set -- `trap`"
$ echo "$@"
trap -- echo 'hoge' EXIT
```
一見成功しているようだが、trapの出力内にエスケープされない改行が
あるため、EXIT以外のシグナルがsetの引数にならずに、trapコマンド
として実行されてしまっている。

```
$ set -x
$ eval "set -- `trap`"
++ trap
+ eval 'set -- trap -- '\''echo '\''\'\'''\''hoge'\''\'\'''\'''\'' EXIT
trap -- '\''echo "EXIT
trap --"'\'' INT'
++ set -- trap -- 'echo '\''hoge'\''' EXIT
++ trap -- 'echo "EXIT
trap --"' INT
```
しかし、trapの出力全体をクォートすると、今度はシングルクォートの
パースができない。

一度echoに渡して改行を潰すという手も考えたが、
クォート内の改行も潰れるためうまく行かない。

```
$ eval "set -- $(echo `trap`)"
$ echo $@
trap -- echo 'hoge' EXIT trap -- echo "EXIT trap --" INT
```

目下逐次実行したいのは1行目のEXITなのでsetでなんとかなる気もするが、
よく考えたらdashだとSIGINT等で脱出する場合はEXITに該当しないので
汎用的な方法を考えるべきだろう。

## dashのtrapはコマンド置換できない
実は上記のコードの一部はdashでは実行できない。
理由はサブシェルではtrapがリセットされてしまい、
コマンド置換ができないためだ。

```
$ trap echo 0
$ trap
trap -- 'echo' EXIT
$ echo "`trap`"

$ 
```
なんでevalできる形式で出力したのか不明な仕様である。
※この問題ついてのPOSIXの仕様は[ここ](https://yash.osdn.jp/doc/ja/_trap.html)が詳しい。

半ば諦めながらよい対処を探していたら[この記事](https://unix.stackexchange.com/questions/343990/dash-how-to-capture-output-of-trap-invoked-w-o-arguments)を見つけた。
一時ファイルを作るのは本末転倒感がすごいが、仕方がないので、
以下の方法でコマンド置換を行う。

```bash
_get_trap() {
  rm $1
  trap
  eval $2'="`cat`"'
} >$1 <$1
alias get_trap='_get_trap /tmp/gettrap.$$'
```
※$$はサブシェルでも同じ値なので、特に並列実行するのならちゃんとmktempを使うか真面目にプロセスIDを取りに行こう。

## dash版 trap -p

setを使うのは諦めて、trapの出力をevalでそのまま実行する。
ただし、trapをダミーの関数とaliasで上書きし、引数としてevalが行った
パースの結果を取得する。(evalがエスケープを外してしまうので、
再出力時に再エスケープが必要だ。esc関数の定義は[ここ](https://qiita.com/opiliones/items/d8c9299e0b7c4e5830c5)を参照。)
※aliasを使うのはdashがtrapという関数名をエラーにするため

```bash
_trap() {
  local RET
  case $4 in $1)
    esc "$3"
    echo trap -- "$RET" $4
  esac
}

trap_p() {
  local traps
  get_trap traps

  alias trap="_trap $1"
  eval "$traps"
  unalias trap
}
```
当初の予定より長い道のりだったが、ここにdash版trap -pが完成した。

```
$ trap_p EXIT
trap -- 'echo '\''hoge'\''' EXIT
$ trap_p INT
trap -- 'echo "EXIT
trap --"' INT
```

## trapの逐次実行

やっと逐次実行だ。
もう記事を書くのも面倒なので結論を書く。
上記のtrap_pを改造すればできる。(なぜtrap_pを作ったんだったっけ...)

```bash
_trap_push() {
  case $5 in $2)
    trap -- "$1;$4" $5
  esac
}

trap_push() {
  local traps
  get_trap traps

  alias trap="_trap_push '$1' $2"
  eval "$traps"
  unalias trap
}
```
```
$ trap
trap -- 'echo '"'"'hoge'"'" EXIT
trap -- 'echo "EXIT
trap --"' INT
$ trap_push 'echo "fuga"' EXIT
$ trap
trap -- 'echo "fuga";echo '"'"'hoge'"'" EXIT
trap -- 'echo "EXIT
trap --"' INT
$ exit
fuga
hoge
```
実は冒頭に示した機能はこれでは完成しておらず、
途中に書いたとおり複数のsignalを同時に設定できる機能が必要だが、
難しい問題ではないので記事はここで終わりにする。