direnvのコマンド direnv hook bash で出力されるスクリプトについて調べてみた

以前、以下記事で**direnvのインストール手順 hook direnv into your shell. について**調べてみました。

https://qiita.com/tmori97840/items/1c99084399dda26e815c

その際、direnvのhookコマンドで出力される**Bash用のフックを設定するためのスクリプト**が気になったため、少し調べてみました。

# direnv hook bash でどんなスクリプトが出力されるの？
Macのターミナルで`direnv hook bash`を実行し、スクリプトを出力してみました。

```shell:
% direnv hook bash

_direnv_hook() {
  local previous_exit_status=$?;
  trap -- '' SIGINT;
  eval "$("/opt/homebrew/bin/direnv" export bash)";
  trap - SIGINT;
  return $previous_exit_status;
};
if [[ ";${PROMPT_COMMAND[*]:-};" != *";_direnv_hook;"* ]]; then
  if [[ "$(declare -p PROMPT_COMMAND 2>&1)" == "declare -a"* ]]; then
    PROMPT_COMMAND=(_direnv_hook "${PROMPT_COMMAND[@]}")
  else
    PROMPT_COMMAND="_direnv_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
  fi
fi
```

いくつかわからない処理があったので、以下の要素に分解して調べてみましょう。

- local
- trap
- PROMPT_COMMAND

### localとは
出力されたスクリプトの`_direnv_hook()`内で使われており、Bashのマニュアル**4.2 Bash Builtin Commands**に記載があります。
```shell:_direnv_hook()
_direnv_hook() {
  local previous_exit_status=$?;
  〜省略〜
  return $previous_exit_status;
};
```
> `local [option] name[=value] …`
For each argument, a local variable named name is created, and assigned value. The option can be any of the options accepted by declare. local can only be used within a function; it makes the variable name have a visible scope restricted to that function and its children.

「localは関数内でのみ使用可能で、変数名の可視スコープをその関数とその子関数に限定する。」
とあることから、**関数内でのみ有効な変数を宣言**することができ、宣言した変数が別の関数やスクリプト全体に影響を与えることを防ぐことができます。

### trapとは
出力されたスクリプトの`_direnv_hook()`内で使われており、Bashのマニュアル**4.1 Bourne Shell Builtins**に記載があります。
```shell:_direnv_hook()
_direnv_hook() {
  〜省略〜
  trap -- '' SIGINT;
  〜省略〜
  trap - SIGINT;
  〜省略〜
};
```
> `trap [-lp] [arg] [sigspec …]`
The commands in arg are to be read and executed when the shell receives signal sigspec. If arg is absent (and there is a single sigspec) or equal to ‘-’, each specified signal’s disposition is reset to the value it had when the shell was started. If arg is the null string, then the signal specified by each sigspec is ignored by the shell and commands it invokes.

「arg のコマンドは、シェルがシグナル sigspec を受け取ったときに読み込まれ、実行される。 arg が存在しない (そして sigspec が 1 つである) か、'-' に等しい場合、指定された各シグナルの処分は、シェルが起動されたときの値にリセットされる。 arg が NULL 文字列である場合、各 sigspec で指定されたシグナルは、シェルとそれが呼び出すコマンドによって無視される。」
とあることから、`trap -- '' SIGINT`で**SIGINTを無視し**、`trap - SIGINT`で**その無視をリセット**しています。

:::note info
--について
引数'--'を与えることがでオプション処理を終了させ、次の単語がオプション以外の引数であることを示し、それ以外は無視されます。これは、コマンドの引数が'-'で始まる場合に便利です。
:::
:::note info
SIGINTについて
シグナル（プロセスに送信される信号）の一つで、「キーボードからの割り込み、control + c」をプロセスに要求します。
:::

### PROMPT_COMMANDとは
出力されたスクリプトのif文で使われており、Bashのマニュアル**5.2 Bash Variables**に記載があります。
```shell:
if [[ ";${PROMPT_COMMAND[*]:-};" != *";_direnv_hook;"* ]]; then
  if [[ "$(declare -p PROMPT_COMMAND 2>&1)" == "declare -a"* ]]; then
    PROMPT_COMMAND=(_direnv_hook "${PROMPT_COMMAND[@]}")
  else
    PROMPT_COMMAND="_direnv_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
  fi
fi
```
> PROMPT_COMMAND
If this variable is set, and is an array, the value of each set element is interpreted as a command to execute before printing the primary prompt ($PS1). If this is set but not an array variable, its value is used as a command to execute instead.

「この変数が設定されていて配列である場合、設定されている各要素の値は、 主プロンプト($PS1)を表示する前に実行するコマンドとして解釈される。 この変数が設定されているが配列変数でない場合は、 その値が代わりに実行するコマンドとして使われる。」
とあることから、変数PROMPT_COMMANDに関数を設定することで、**主プロンプト($PS1)を表示する前に関数を実行する**ことができます。

また、`if [[ ";${PROMPT_COMMAND[*]:-};" != *";_direnv_hook;"* ]]; then`で**PROMPT_COMMANDに_direnv_hookが存在するか確認**し、存在しない場合は**PROMPT_COMMANDに_direnv_hookを設定**しています。

IFS変数が空の場合、;区切りで展開されない
macOSの場合、配列ではなくテキストなので問題ない
Linux環境での挙動を試してみる
direnvでバグを見つけた場合の修正依頼はどうすればいい？

### direnv hook bash で出力されるスクリプトがしていること
- BashのPROMPT_COMMANDに_direnv_hookが存在するか確認し、存在しない場合に設定
- _direnv_hookでは`trap -- '' SIGINTでSIGINT`を無視し`direnv export bash`を実行、`trap - SIGINT`でその無視をリセット

# まとめ
いかがでしたでしょうか？

`direnv hook bash`でどんなスクリプトが出力されているかだいぶイメージできるようになりました。

_direnv_hookで実行している`direnv export bash`も少し気になりますが、**export**とあることから環境変数を有効にしているのだろうと理解しました。

**direnv**について詳しく調べることで、ますますdirenvを使いたくなりました。
今後もどんどん使っていきたいと思います。