bashの .bash_profile と .bashrc の挙動の整理と使い分け方

新しい開発環境を構築していて、 `.bashrc` と `.bash_profile` のどちらに何を書くべきなのか、今までは適当にやっていたがベストプラクティスを調べて準拠したくなり、気合を入れて調べたのでメモを残す。（macOS Venturaで確認）

# 調べたいこと

* bashの主要な設定ファイルである `.bashrc` と `.bash_profile` は、それぞれどんなときに読み込み・実行されるのか確認し、整理する
* 上記整理に従って、bashの設定の管理指針の明確化を図る

# bashシェルのモード

bashのシェルには、起動方法によって「**ログインシェル**」と「**インタラクティブシェル**」があり、それぞれ読み込まれる設定ファイルが異なる。

「**ログインシェル**」は、主にシェルのセッション開始時に用いられる。具体的には、macOSでターミナルを立ち上げたり、SSHでリモートログインしたときなどである。一番「外側のシェル」と考えておけば良いだろう。逆に、シェルの中でさらに`bash`コマンドなどでシェルを開始した場合などは「**非ログインシェル**」となる。

「**インタラクティブシェル**」は、ユーザーがインタラクティブに操作可能な（具体的には標準入力と標準エラー出力がターミナルに接続された）状態で起動したシェルを指す。逆に、`bash -c date` や `bash foo.sh` のように、コマンドやシェルスクリプトを実行する場合は「**非インタラクティブシェル**」となる。

これらふたつの概念は直交しているので、ログインシェルかつインタラクティブシェル（**インタラクティブ・ログインシェル**）、あるいは非ログインシェルかつ非インタラクティブシェル（**非インタラクティブ・非ログインシェル**）という状態も取りうる。

## 確認方法

そのシェルが **ログインシェル** であるかどうかは、`shopt -q login_shell` の終了コードが0かどうかで判別できる。また、**インタラクティブシェル** であるかを確認するには、bashの起動フラグが格納されている `$-` 変数に `i` が含まれているかどうかで判別できる。

```bash
$ bash --login -i      # ログインシェルかつインタラクティブシェルでbashを起動
$ echo $-
himBH                  # iが含まれている => インタラクティブシェル
$ shopt -q login_shell
$ echo $?
0                      # 直前の終了コードが0 => ログインシェル
```


# 各起動モードでの設定ファイルの実行パターン

さて、manによれば、上記のそれぞれのシェルでの設定ファイルの読み込み・実行パターンは以下の定義されている。

| モード | 設定ファイルの実行 |
|:-:|:-|
| **インタラクティブ・<br>ログインシェル**  | ① /etc/profile を実行する<br>② ~/.bash_profile, ~/.bash_login, ~/.profile の順で最初に見つかったファイルひとつを実行する |
|  **インタラクティブ・<br>非ログインシェル** |  ~/.bashrc を実行する |
|  **非インタラクティブシェル** |  デフォルトでは何も実行しない（$BASH_ENV があれば、指定された名前のファイルを実行する） |

各設定ファイルごとの早見表にすると、こうなる。

| | インタラクティブ・ログインシェル | インタラクティブ・非ログインシェル | 非インタラクティブシェル |
|:-|:-:|:-:|:-:|
| /etc/profile  | ○ |   |   |
| ~/.bash_profile<br>~/.bash_login<br>~/.profile<br>いずれかひとつ  | ○ |   |   |
| ~/.bashrc  |   | ○  |   |
| $BASH_ENV |   |   | ○  |

## アプリケーション経由でbashを起動するときの挙動

次に、各アプリケーションや環境でbashを使った時に、実際にどのモードが使われるか確認していく。

### macOSのTerminal.app

macでターミナルを立ち上げると、 **インタラクティブ・ログインシェル** が開始する。読み込まれるのは `.bash_profile`。

### SSH

#### SSHリモートログイン

SSHでリモートホストにログインすると、リモートホストのbashは **インタラクティブ・ログインシェル** が開始する。読み込まれるのは `.bash_profile`。

#### SSHリモートコマンド/SCP

SSHリモートコマンド実行（例: `ssh localhost hostname`、または`scp`）の際にはリモートホストで **非インタラクティブ・非ログインシェル** が開始する。

しかし、bashはリモートシェルでの実行を検知すると `.bashrc`を実行する仕様になっている。

> (man bash)
> Bash attempts to determine when it is being run by the remote shell daemon, usually rshd. If bash determines it is being run by rshd, it reads and executes commands from ~/.bashrc, if that file exists and is readable.
>
> Bashは、リモートシェルデーモン（通常はrshd）によって実行されているかどうかを判断しようとします。bash が rshd によって実行されていると判断した場合、~/.bashrc が存在し、そのファイルが読める場合は、そのファイルからコマンドを読み込んで実行する。

この仕様によって、 `.bashrc`にechoが含まれていると[問題が起きる](https://qiita.com/montblanc18/items/b93fa4082e3bc2702a7f)ので注意すること。

### screen
screenで新規ウィンドウを作成すると、 **インタラクティブ・ログインシェル**が開始する。実行されるのは `.bashrc`である。

### tmux

tmuxで新規ペインを作成すると、デフォルトでは **インタラクティブ・ログインシェル** が開始する。そのため、実行されるのは `.bash_profile` である。

後で述べるように、この挙動はやや都合が悪いので、 `.tmux.conf` に以下の設定を追加することで **インタラクティブ・非ログインシェル** に変更することができる。こうすると `.bashrc`が実行されるようになる。

```.tmux.conf
set -g default-command "${SHELL}"
```

# 管理方針

ここまでで、bashの主な起動モードや実行される設定ファイル、各アプリケーションからシェルを立ち上げたときの挙動を大まかに確認できた。

| | インタラクティブ・ログインシェル (.bash_profile) | インタラクティブ・非ログインシェル(.bashrc) | 非インタラクティブシェル(実行なし) |
|:-:|:-:|:-:|:-:|
| macOS Terminal新規ウィンドウ | ○  |   |   |
| GNOMEデスクトップログイン時([らしい](https://qiita.com/dark-space/items/cf25001f89c41341a9fd)) | ○  |   |   |
| SSHリモートログイン | ○  |   |   |
| SSHリモートコマンド,SCP  |   | ○(※1) |   |
| bash（シェル起動）  |   | ○ |   |
| bash（コマンド実行）  |   |   | ○ |
| screen  |  | ○  |   |
| tmux | ○<br>(デフォルト時)  | ○<br>(default-command設定時)  |   |

※1: モード自体は非インタラクティブだが、前述の通りbashのリモートシェル仕様によってインタラクティブ相当の動作をする

では、これを踏まえてbashの設定ファイルの管理方針を検討しよう。

一般的に、`.bash_profile`にはそのセッションにおいて一度だけ実行したい設定を、 `.bashrc` にはシェルを開始するごとに毎回実行したい設定を記述するべきと言われている。

シェルの中でシェルを開始した場合、環境変数は内側のシェルに引き継がれる。そのため、シェルを起動するごとに実行される `.bashrc`に `export PATH=/path/to/bin:$PATH`といった環境変数の追加を記述してしまうと、シェルを立ち上げるごとにPATHが長くなってしまう。それで誤動作が起きるケースは稀だが、ともあれそういった理由で環境変数の設定の記述は `.bash_profile` が適しているとされている。

一方で、シェルのエイリアスや関数の定義などは、内側のシェルには引き継がれない。これらの設定を ログインシェルでしか実行されない `.bash_profile` に記述してしまうと、内側のシェルでエイリアスなどが使えなくなってしまうので、エイリアスや関数は `.bashrc`で定義するのが適しているとされている。

この一般的な方針を採用しても問題なさそうである。ただし、二つだけ注意することがある。ひとつは、単純にそのように書き分けただけではターミナルを立ち上げた（ログインシェル）際に `.bashrc` が実行されず、エイリアスや関数が定義されないので、`.bash_profile` から `.bashrc` をsourceしてやる必要がある。

```.bash_profile
[[ -f ~/.bashrc ]] && source ~/.bashrc
```

もう一つはtmuxである。先ほど少し述べたように、デフォルトではtmuxは**インタラクティブ・ログインシェル**を起動してしまう。しかし、ユーザーは通常 bash などのシェルから tmux を開始するので、これではログインシェルが2階層になってしまう。これによって、本来シェルのセッション中一度だけ実行したい `.bash_profile` が2回実行され、PATHが長くなってしまうなどの問題として現れる。そこで、**インタラクティブ・ノンログインシェル** を起動させるため、 `tmux.conf` で設定を変更する必要がある。

```.tmux.conf
set -g default-command "${SHELL}"
```

これで「.bash_profileはセッション開始時に最初に一度だけ実行される、.bashrcはシェル開始時に毎回実行される」という原則へのコンプライアンスが得られた。

# まとめ

`.bash_profile`はセッション開始時に一度だけ実行される。 `.bashrc`はシェル開始時に毎回実行される。（ただしtmuxのように一部この原則に準拠しないアプリケーションがあるので対処が必要）

本稿で私が勧めるbashの設定ファイルの管理方針は、`.bash_profile`で環境変数を設定して`.bashrc`をsourceする、 `.bashrc` でエイリアスや関数を設定すること