alpine linux / ash でも ashrc/bashrc(.*rc) が欲しい! (設定ファイル類の解説つき)

Docker コンテナ等でよく使われる alpine linux. そのログインシェルである `ash` の設定ファイルについてです。   

私自身 Docker 以外で alpine linux をよく使っています。(参考: [iSH](https://ish.app/))  
Docker を前提とした解決方法以外で、設定ファイルについて書かれた日本語の記事が(私が探した限りでは)なかったので記述します。

# 忙しい人向け

- `ash` には**デフォルトで `.bashrc` に相当する対話シェル用の設定ファイルはありません**。
- `$HOME/.profile` を作成し、その中に以下のものを記述することで、`.bashrc` 相当のファイル(以下だと `.ashrc` という名前)を作ることができます。(対話シェル用設定ファイルの名前は`.ashrc`である必要はなく、任意の名前が使えます。)
- ただしこれだと対話シェル以外(シェルスクリプト実行時など)にも読み込まれるため、一工夫が必要です(後述)。

```sh:.profile
ENV=$HOME/.ashrc; export ENV
```


# 解説

 ~~結構探し回ったのですが、`ash` の man に答えがちゃんとありました。 (`iSH` にはそもそも `man` がデフォルトで存在しないので見逃してた)~~

ネット上以外で `man ash(1)` を手に入れることができなかったので、真偽は定かではありません。   

~~ただし `man busybox(1)` は docker の alpine コンテナで手に入ったのでそれを見ようと思います。~~  
と思ったら、 busybox(1) の man と以下の文言は全く別のものだった(どちらかと言ったら `dash` の方が近い)。 ただ `ash --help` と `busybox --help` の出力が違うのは謎。`readlink -f $(command -v ash)` と `readlink -f $(command -v busybox)` はどちらも `/bin/busybox` なのに ...

ですがとりあえず各所で根拠として挙げられている `ash(1)` の `man` ページを示します。

[ash(1)](https://linux.die.net/man/1/ash)

> ### Invocation
>
> If no args are present and if the standard input of the shell is connected to a terminal (or if the -i flag is set), and the -c option is not present, the shell is considered an interactive shell. An interactive shell generally prompts before each command and handles programming and command errors differently (as described below). When first starting, the shell inspects argument 0, and if it begins with a dash '-', the shell is also considered a login shell. This is normally done automatically by the system when the user first logs in. A login shell first reads commands from the files /etc/profile and .profile if they exist. If the environment variable ENV is set on entry to a shell, or is set in the .profile of a login shell, the shell next reads commands from the file named in ENV. Therefore, a user should place commands that are to be executed only at login time in the .profile file, and commands that are executed for every shell inside the ENV file. To set the ENV variable to some file, place the following line in your .profile of your home directory
> 
>     ENV=$HOME/.shinit; export ENV
> 

意訳:

> ### 起動
>
> 引数なしでシェルが起動され、かつ標準入力がターミナルに接続されていたら(もしくは `-i` オプションが設定されている)、かつ `-c` オプションでない場合、シェルはインタラクティブシェル(訳注: [対話モードとログインシェルとは](#対話モードとログインシェルとは) 参照)だよ。インタラクティブシェルは一般的にプロンプトがあり、プログラミングやエラーハンドリングが異なるよ。
> はじめにシェルは0番目の引数(訳注: `$0`, 自分自身の文字列のこと) を見るよ。`-` で始まってたらログインシェルだよ。この処理はシステムにより最初のログイン時に自動で行われるよ。ログインシェルは最初に `/etc/profile` と `.profile` を読むよ。シェル実行時には `ENV` 環境変数があれば(もしくは `.profile` 中で設定されれば)、シェルは次にその `ENV` 環境変数で指定されたファイルを読むよ。よってユーザは **ログイン時に実行したいものは `.profile` に**、**シェル実行時に毎回実行したいものは `ENV` で指定されたファイルに書くべきだよ**。`ENV` 環境変数を設定して実行させる場合には `.profile` に以下のように書いてね。  
>
>     ENV=$HOME/.shinit; export ENV
>

というわけなので **ログイン時に実行したいものは `.profile` に**、**シェル実行時に毎回実行したいものは `ENV` で指定されたファイルに書きましょう(ただし実際には違う挙動をする(後述))**。

しかしこの章には続きがあります。

[ash(1)](https://linux.die.net/man/1/ash)
> [省略] Since the ENV file is read for every invocation of the shell, including shell scripts and non-interactive shells, the following paradigm is useful for restricting commands in the ENV file to interactive invocations. Place commands within the ''case'' and ''esac'' below (these commands are described later):
> 
>     case $- in *i*)
>     # commands for interactive use only
>     ...
>     esac

意訳: 
> `ENV` で指定されたファイルはどんなシェルの呼び出しでも読まれるよ。それにはシェルスクリプトや非対話シェルも含まれるよ。なのでインタラクティブな時だけ実行するなら以下のやり方をお勧めするよ ...

ですがこの挙動は手もとでは確認できませんでした。なので基本は case 句で囲わなくて良いかもしれません。

ただし、検証の結果、`.profile` は対話シェルかつログイン時でのみ実行されるのに対して、 `ENV` で指定したファイルは **対話シェルであればログインシェル・非ログインシェル両者** の場合に読み込まれることがわかりました。つまり `.bashrc` を `.profile` で読み込んだときと同じ挙動をすることがわかりました。

これらの挙動は `sh` と似ていると言えるのではないでしょうか。

[man sh(1)](https://manpages.org/sh)
> ## ENVIRONMENT VARIABLES
> The following environment variables shall affect the execution of sh:
> 
> ### ENV
> This variable, when and only when an interactive shell is invoked, shall be subjected to parameter expansion (see Parameter Expansion ) by the shell, and the resulting value shall be used as a pathname of a file containing shell commands to execute in the current environment. The file need not be executable. If the expanded value of ENV is not an absolute pathname, the results are unspecified. ENV shall be ignored if the real and effective user IDs or real and effective group IDs of the process are different.


<details>
<summary>挙動の検証</summary>

挙動の確認には `.ashrc` / `.profile` に以下のように書き込み、以下を実施した後、`printenv` で確認しました。

```sh:.ashrc
DOT_ASHRC="$(date)"
export DOT_ASHRC
```

```sh:.profile
DOT_PROFILE="$(date)"
export DOT_PROFILE
```

- 実施したこと
    - `ash -c true` => `DOT_PROFILE`/`DOT_ASHRC` 両者変化なし。
    - `hello` ファイルを作成し `$ ash ./hello` / `$ ./hello` を実行した => `DOT_PROFILE`/`DOT_ASHRC` 両者変化なし。
        -   ```sh:hello
            #!/bin/sh
            echo "hello"
            ```
    - `$ ash` を実行した (`対話シェル` かつ `非ログインシェル`) => `DOT_ASHRC` のみ変化
    - 非ログインシェルの状態で先ほどの `hello` を実行する(`$ ./hello` / `$ ash ./hello`) => `DOT_PROFILE`/`DOT_ASHRC` 両者変化なし。

環境変数`DOT_ASHRC`は変化せず、非対話シェルで実行したものでは `.ashrc` は読み込まれていませんでした。

ただし `$ ash` を実行した時のように `対話シェル` かつ `非ログインシェル` では実行されていました。(`.profile` は実行されず、`DOT_PROFILE`は変化しなかった。)

これらは以下の環境変数で実証しました。
```sh
# iSH

$ cat /etc/os-release 
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.14.3
PRETTY_NAME="Alpine Linux v3.14"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
```
```sh
# docker alpine:3.14

$ cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.14.10
PRETTY_NAME="Alpine Linux v3.14"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
```

</details>

# そもそもの設定ファイル類について(おまけ)

まずそもそもの設定ファイル類について `bash` を例に簡単に説明します。  
そんなもん要らん! という人は読み飛ばしてください。

## 種類と読み込み順

主な設定ファイルには `.bashrc` と `.bash_profile`, `.profile`があり、それぞれ違いがあります。これらのファイルの中にシェルプログラムを書き込むことで、指定されたときに読み込み(大概何かの開始時)。設定をすることができます。

- **`~/.bashrc`** は**対話モード**かつ**非ログインシェル**の bash を起動する時に毎回実行。
    - `$ bash` のように新たに対話シェルで bash を実行するときのみ実行する。逆にそれ以外で `bash` 自身が読み込むことはない。(**ログイン時に自動的に読み込まれるわけではない**)

- **ログイン時**には `/etc/profile/` というファイルと `/etc/profile.d` というディレクトリ(の直下のファイル群)があり、それらが最初に読み込まれる。 
    - 次に bash は `~/.bash_profile` -> `~/.bash_login` -> `~/.profile` の順番で探し、**一番最初に見つかった時点で探索を終了し** そのファイルを読み込む
        - なのでこれらに `.bashrc` を読み込む設定を書く必要がある。(デフォルトで書かれていることもある)
        - **`~/.profile`** に関しては `bash` 以外も使用するもの
            - なのでシェルの種類によらないスクリプトを記述する
    - あと `/etc/bash.bashrc` というファイルもある


よってまとめると以下のような順になる


```sh
diagrams:
       login                   +-----------------+
         |                     | must be read    |
  +--------------+             +-----------------+
  | /etc/profile |             +*****************+
  +--------------+             | may not be read |
         |                     +*****************+
 +----------------+
 | /etc/profile.d | <--- all `*.sh` files are loaded
 +----------------+
         |                       +***************+
if .bash_profile exists? ==Yes=> | .bash_profile |  + + +
         | No                    +***************+      +
         |                                              v
         V                       +*************+        +
if .bash_login exists?   ==Yes=> | .bash_login |  + + + +
         | No                    +*************+        +
         |                                              v
         V                                              +
    +**********+                                        +
    | .profile | <- + + + +  may load + + + + + + + + + +
    +**********+                                        +
                                                        +
                                                        +
                                                        v
                                                        +
    +*********+                                         +
    | .bashrc |  <- + + + +  may load + + + + + + + + + +
    +*********+


 ---------------------------------------------------------
|                                                         |
| start interactive bash process (not a login shell)      |
|        |                                                |
|   +---------+                                           |
|   | .bashrc |                                           |
|   +---------+                                           |
|                                                         |
 ---------------------------------------------------------
```


## `対話モード`と`ログインシェル`とは
man BASH(1) には以下のようにあります。

> A  _login shell_ is one whose first character of argument zero is a -, or one started with the --login option.
> An _interactive shell_ is one started without non-option arguments (unless -s is  specified)  and  without the  -c  option  whose  standard  input  and  error  are  both  connected to terminals (as determined by isatty(3)), or one started with the -i option.  PS1 is set and $- includes i if bash is interactive, allowing a shell script or a startup file to test this state.

日本語版:

> ログインシェル(login shell)とは、0 番目の引き数の最初の文字が - であるシェル、または --login  オプション付きで起動されたシェルのことです。
> 対話的なシェルとは、 オプションでない引き数がなく、 標準入力と標準エラー出力がいずれも端末に接続されていて (これは isatty(3) で調べられます)、 -c オプションが指定されていない状態で起動されたシェル、または  -iオプション付きで起動されたシェルのことです。  bash  が対話的に動作している場合には、 PS1 が設定され、 $- に i が含まれます。 これを利用すると、対話的動作の状態であるかどうかを、  シェルスクリプトや起動ファイルの内部で調べられます。

ざっくりいうと
- `対話的`かつ`ログインシェル`: ターミナルに入ったときや `su - <username>` でユーザにログインした時、 `ssh` でリモートの端末にログインした時など。
- `対話的`かつ`非ログインシェル`: `su <username>` のように `su` コマンドに `-` を付けずにログインした時や、ログイン後にユーザが明示的に `bash` を実行した時など。
    - ただしこの場合でも `bash -l (or --login)` や `exec -l bash` などとするとログインシェルとして起動する
- `非対話` かつ `ログインシェル`: 不明、そんなことあるのか? ただ、`bash -l (or --login) <script-file>`で実現することはできた。 
- `非対話` かつ `非ログインシェル`: これらはスクリプトファイルを実行するときなどが当たる。

見分け方
- `ログインシェル`or`非ログインシェル`: 
    - `$ shopt login_shell` を実行し、`login_shell     on` と出ればログインシェル。 `login_shell     off` とでれば非ログインシェル。ただしこれば bash 限定。
    - またインタラクティブ状態で `echo $0` を実行し、 `-bash` のように先頭に `-` があればログインシェル。ただし `bash -l` などでログイン後に明示的にログインシェルとして起動したときは先頭に `-` が付与されない(`exec -l bash` の場合は付与される)。これは `su -` や `exec -l` が bash の引数 0 つまり bash 自身に `-` を付与するため。(`exec` や `su` の `man` 参照)
- `対話` or `非対話`:
    - プロンプト(`$PS1`) があれば大概は対話シェルである。
    - `echo $-` で出力された文字列に `i` があれば対話シェルである。 

参考文献:
- "bash の初期化ファイル .profile, .bashrc, .bash_profile の使い分けと管理方針 - A Memorandum".     
	[https://blog1.mammb.com/entry/2019/12/01/090000](https://blog1.mammb.com/entry/2019/12/01/090000) 

- "105.1 レッスン 1 | LPI Learning".     
	[https://learning.lpi.org/ja/learning-materials/102-500/105/105.1/105.1_01/](https://learning.lpi.org/ja/learning-materials/102-500/105/105.1/105.1_01/) 

 - "30 シェルについての勉強 -- ログイン・シェルとインタラクティブ・シェル".     
	[https://m-katsurada.sakura.ne.jp/knowhow-2023/node30.html](https://m-katsurada.sakura.ne.jp/knowhow-2023/node30.html) 

 - "ログインシェル、インタラクティブシェルの見分け方(bash) - 朝から昼寝(こちらは旧URLです)".     
	[https://happy-nap.hatenablog.com/entry/2022/12/04/112257](https://happy-nap.hatenablog.com/entry/2022/12/04/112257) 

 - MAN BASH(1)
 - MAN SU(1)


## 設定ファイル類のベストプラクティス

複数の参考文献を見るに以下のような使い分けが筋が通っていて合意できるでしょう。   

| ファイル名 | 使い分け | 例 | 
| :-------: | ------- | -- |
| `~/.profile` | `bash` 以外でも使い、かつ対話シェル以外で使うものをかく。ウェルカムメッセージ(ログインメッセージ)を出力してもいい(`/etc/profile.d` に書くことが多い?) | `環境変数` 等 |
| `~/.bash_profile` | `~/.bashrc` と `/.profile` を読みこむだけにする。(ここで読まないと両者は読まれない) | なし |
| `~/.bash_login` | 使わない | なし |
| `~/.bashrc` | 対話シェルで使うもの。ここでは何も出力してはならないらしい。 | `エイリアス` `自作コマンド` `$PS1` 等

参考文献:

- "Bash: .bashrcと.bash_profileの違いを今度こそ理解する｜TechRacho by BPS株式会社".     
	[https://techracho.bpsinc.jp/hachi8833/2021_07_08/66396](https://techracho.bpsinc.jp/hachi8833/2021_07_08/66396) 

 - "bash - Difference between .bashrc and .bash_profile - Super User".     
	[https://superuser.com/questions/183870/difference-between-bashrc-and-bash-profile](https://superuser.com/questions/183870/difference-between-bashrc-and-bash-profile) 
 
 -  "unix - Choosing between .bashrc, .profile, .bash_profile, etc - Super User".     
	[https://superuser.com/questions/789448/choosing-between-bashrc-profile-bash-profile-etc](https://superuser.com/questions/789448/choosing-between-bashrc-profile-bash-profile-etc) 



# 参考文献
- "bash の初期化ファイル .profile, .bashrc, .bash_profile の使い分けと管理方針 - A Memorandum".     
	[https://blog1.mammb.com/entry/2019/12/01/090000](https://blog1.mammb.com/entry/2019/12/01/090000) 
- "[Linux]環境変数の読み込み順番 #Bash - Qiita".     
	[https://qiita.com/yunzeroin/items/480a3a677f78a57ac52f](https://qiita.com/yunzeroin/items/480a3a677f78a57ac52f) 
- "Linuxで環境変数が保存される仕組み(bash_profile) #Bash - Qiita".     
	[https://qiita.com/tomo0/items/674e06da84c921f5407a](https://qiita.com/tomo0/items/674e06da84c921f5407a) 


- "105.1 レッスン 1 | LPI Learning".     
	[https://learning.lpi.org/ja/learning-materials/102-500/105/105.1/105.1_01/](https://learning.lpi.org/ja/learning-materials/102-500/105/105.1/105.1_01/) 

 - "30 シェルについての勉強 -- ログイン・シェルとインタラクティブ・シェル".     
	[https://m-katsurada.sakura.ne.jp/knowhow-2023/node30.html](https://m-katsurada.sakura.ne.jp/knowhow-2023/node30.html) 

 - "ログインシェル、インタラクティブシェルの見分け方(bash) - 朝から昼寝(こちらは旧URLです)".     
	[https://happy-nap.hatenablog.com/entry/2022/12/04/112257](https://happy-nap.hatenablog.com/entry/2022/12/04/112257) 

 - "ash(1) - Linux man page"
     [https://linux.die.net/man/1/ash](https://linux.die.net/man/1/ash)


- "Bash: .bashrcと.bash_profileの違いを今度こそ理解する｜TechRacho by BPS株式会社".     
	[https://techracho.bpsinc.jp/hachi8833/2021_07_08/66396](https://techracho.bpsinc.jp/hachi8833/2021_07_08/66396) 

 - "bash - Difference between .bashrc and .bash_profile - Super User".     
	[https://superuser.com/questions/183870/difference-between-bashrc-and-bash-profile](https://superuser.com/questions/183870/difference-between-bashrc-and-bash-profile) 
 
 -  "unix - Choosing between .bashrc, .profile, .bash_profile, etc - Super User".     
	[https://superuser.com/questions/789448/choosing-between-bashrc-profile-bash-profile-etc](https://superuser.com/questions/789448/choosing-between-bashrc-profile-bash-profile-etc) 

 - MAN BASH(1)
 - MAN SU(1)