「bash」、「source」といったシェルスクリプトの実行方法の違いを紐解いてみる

[「LinuCレベル1」の試験範囲](https://linuc.org/linuc1/range/)に「シェルおよびスクリプト」という章がありまして、その中でシェルスクリプトを実行する場合は以下の構文が利用できます、といった説明があります。

```
$ . スクリプトファイル名
$ source スクリプトファイル名
$ bash スクリプトファイル名
$ ./スクリプトファイル名
```

`.` と `source` についてはシェルスクリプトの実行というよりも、 `~/.bash_profile` や `~/.profile` といったファイルの再読み込みの場合に利用する機会が多いので、「シェルスクリプトを実行している」という意識では利用していませんでした。
そこで、今回はこれらの実行方法の違いについて紐解いてみようと思います。

---

### 今回の動作確認環境
- Vagrant 2.2.14 
- CentOS 8

---

### `. スクリプトファイル名` と `source スクリプトファイル名` の違い

まず、 `.` と `source` についてですが `man bash` 内の `SHELL BUILTIN COMMANDS` を見ると、以下のように同列に記載されていて、「現在のシェル環境でファイルを読み込んで実行する」と説明されています。このことから `.` と `source` に違いはなく、同等のコマンドであると考えてよさそうです。（ `.` および `source` は `alias` 、 `cd` などと同じくbashのビルトインコマンドなので `man builtins` でもマニュアルが参照可能です。）

```
SHELL BUILTIN COMMANDS
       〜省略〜
        .  filename [arguments]
       source filename [arguments]
              Read and execute commands from filename  in  the  current  shell
              environment  and return the exit status of the last command exe-
              cuted from filename.  If filename  does  not  contain  a  slash,
              filenames  in  PATH  are  used  to find the directory containing
              filename.  The file searched for in PATH need not be executable.
```

`man bash` 内で言及されていることから、bashの場合は `source` が利用可能だとわかりますが、その他のシェルでは使えないこともあります。
例として、CentOS8とUbuntu21.10 (Impish Indri)の動作を比較してみます。

#### CentOS8の場合

CentOSのバージョンを確認。

```
$ cat /etc/os-release |grep '^VERSION='
VERSION="8 (Core)"
```

現在のシェルがbashであることを確認。

```
$ echo $0
-bash
```

`source` を実行すると `~/.bash_profile` を読み込むことが可能です。

```
$ source ~/.bash_profile
```

シェルを `/usr/bin/sh` に切り替えて、現在のシェルがshであることを確認します。

```
$ /usr/bin/sh
$ echo $0
/usr/bin/sh
```

`source` を実行しても問題なく読み込めます。

```
$ source ~/.bash_profile
```

理由は `/usr/bin/sh` が `bash` へのシンボリックリンクになっているため `source` が利用可能というわけです。

```
$ ls -l /usr/bin/sh
lrwxrwxrwx. 1 root root 4 May 11  2019 /usr/bin/sh -> bash
```

ちなみに `/usr/bin/sh` を利用している状態で `.` を実行しても `~/.bash_profile` が問題なく読み込まれます。

```
$ echo $0
/usr/bin/sh
$ . ~/.bash_profile
```


#### Ubuntu21.10 (Impish Indri)の場合

Ubuntuのバージョンを確認。

```
$ cat /etc/os-release |grep '^VERSION='
VERSION="21.10 (Impish Indri)"
```

現在のシェルがbashであることを確認。

```
$ echo $0
-bash
```

`source` を実行すると `~/.profile` を読み込むことが可能です。

```
$ source ~/.profile
```

シェルを `/usr/bin/sh` に切り替えて、現在のシェルがshであることを確認します。

```
$ /usr/bin/sh
$ echo $0
/usr/bin/sh
```

`source` を実行するとコマンドが見つからないというエラーが発生します。

```
$ source ~/.profile
/usr/bin/sh: 1: source: not found
```

理由は `/usr/bin/sh` が `dash` へのシンボリックリンクになっていて `dash` では `source` が利用できないからです。

```
$ ls -l /usr/bin/sh
lrwxrwxrwx 1 root root 4 Jul  1 17:34 /usr/bin/sh -> dash
```

`/usr/bin/sh` を利用している状態で `source` は利用できませんでしたが `.` を実行すると `~/.profile` が読み込まれます。

```
$ echo $0
/usr/bin/sh
$ . ~/.profile
```

`.` と `source` は同等のコマンドであること、利用するシェルによっては `source` が利用できない場合があることを踏まえて、以降の動作検証では `.` を利用することとします。

---

### `bash スクリプトファイル名` と `./スクリプトファイル名` の違い

`./スクリプトファイル名` という形式でスクリプトを実行する場合に `shebang` の記述が必要だということは以下の記事でまとめています。

- [シェルスクリプトの中で現在利用しているシェルを確認する方法](https://qiita.com/akase244/items/c3040b68b5e08057c861)


また、 `shebang` に `#!/bin/bash` と記述している場合は `/bin/bash スクリプトファイル名` のように呼び出されているということは、以下の記事でまとめています。

- [シバン(shebang)にシェル以外のコマンドを指定してシバンの動作を確かめてみる](https://qiita.com/akase244/items/c6b4c1a42ab174a3e369)

つまり、 `bash スクリプトファイル名` と `./スクリプトファイル名` は同等であると言えます。このことを踏まえ、以降の動作検証では `bash スクリプトファイル名` を利用することとします。

---

### `. スクリプトファイル名` と `bash スクリプトファイル名` の違い

さて、ここまでを内容をまとめると、どうやら以下の2種類の実行方法の違いを調べれば `source スクリプトファイル名` と `./スクリプトファイル名` の動作についても把握できたと考えてよさそうです。

```
$ . スクリプトファイル名
$ bash スクリプトファイル名
```

#### プロセスIDについて

`. スクリプトファイル名` は `man bash` 内で「現在のシェル環境でファイルを読み込んで実行する」と説明されていましたが、 `bash スクリプトファイル名` については `man bash` 内の　`COMMAND EXECUTION`　にて以下のように記載されています。

```
COMMAND EXECUTION
       〜省略〜
       If this execution fails because the file is not in  executable  format,
       and  the file is not a directory, it is assumed to be a shell script, a
       file containing shell commands.  A subshell is spawned to  execute  it.
       This  subshell  reinitializes itself, so that the effect is as if a new
       shell had been invoked to handle the script, with  the  exception  that
       the  locations  of  commands  remembered  by the parent (see hash below
       under SHELL BUILTIN COMMANDS) are retained by the child.
```

重要なポイントとしては「スクリプトファイルを実行する毎に新たなサブシェル（子プロセス）が生成される」というところかと思います。
文章による説明ではわかりにくいと思うので、プロセスIDに着目してそれぞれの動作の違いを確認してみようと思います。

まずはスクリプト内でプロセスIDを表示する `show_pid.sh` を準備します。

```
$ echo 'echo "PID: $$"' > show_pid.sh
```

現在のシェルのプロセスIDを確認すると `2574` と表示されました。

```
$ echo $$
2574
```

`. show_pid.sh` で呼び出すと「現在のシェル環境でファイルを読み込んで実行する」ので現在のシェルのプロセスIDと同じ値の `2574` が表示されます。

```
$ . show_pid.sh 
PID: 2574
```

この操作を繰り返すと、何度実行しても同じプロセスIDが返ってくることがわかります。

```
$ . show_pid.sh 
PID: 2574
$ . show_pid.sh 
PID: 2574
$ . show_pid.sh
PID: 2574
```

`bash show_pid.sh` で呼び出すと「実行する毎に新たなサブシェル（子プロセス）が生成される」ので現在のシェルのプロセスIDとは異なる値の `2601` が表示されました。

```
$ bash show_pid.sh 
PID: 2601
```

この操作を繰り返すと、　実行する毎に新たなプロセスIDが生成されていることがわかります。

```
$ bash show_pid.sh 
PID: 2603
$ bash show_pid.sh 
PID: 2604
$ bash show_pid.sh 
PID: 2605
```

プロセスIDに着目した `.` と `bash` の動作の違いが理解できたのではないでしょうか。

#### シェル変数と環境変数について

シェル変数は子プロセスに変数の内容が引き継がれませんが、環境変数は子プロセスにも内容が引き継がれます。次はこの動作について見ていきましょう。

まずは現在のシェル環境でシェル変数 `$SHELL_VARIABLE` を定義して、シェル変数を一覧表示する `set` コマンドと変数の内容を表示する　`echo` コマンドで正しく定義されているか確認します。

```
$ SHELL_VARIABLE='This is shell variable!'
$ set |grep SHELL_VARIABLE
SHELL_VARIABLE='This is shell variable!'
$ echo $SHELL_VARIABLE
This is shell variable!
```

次に、環境変数 `$ENV_VARIABLE` を定義して、環境変数を一覧表示する `env` コマンドと　変数の内容を表示する `echo` コマンドで正しく定義されているか確認します。

```
$ ENV_VARIABLE='This is environment variable!'; export ENV_VARIABLE
$ env |grep ENV_VARIABLE
ENV_VARIABLE=This is environment variable!
$ echo $ENV_VARIABLE
This is environment variable!
```

シェル変数 `$SHELL_VARIABLE` と環境変数 `$ENV_VARIABLE` の内容を表示する `show_variables.sh` を準備します。

```
$ cat show_variables.sh 
echo '$SHELL_VARIABLE: '$SHELL_VARIABLE
echo '$ENV_VARIABLE: '$ENV_VARIABLE
```

`. show_variables.sh` で実行すると、同一プロセスのためシェル変数、環境変数ともに表示されます。

```
$ . show_variables.sh 
$SHELL_VARIABLE: This is shell variable!
$ENV_VARIABLE: This is environment variable!
```

`bash show_variables.sh` で実行すると子プロセスで起動するため、シェル変数の値は引き継がれず環境変数のみが引き継がれて表示されます。

```
$ bash show_variables.sh
$SHELL_VARIABLE: 
$ENV_VARIABLE: This is environment variable!
```

#### 現在動作しているシェルへの影響について

最後は、スクリプト内で実行した操作が現在動作しているシェルに対して影響を与えるかについてです。

##### ディレクトリ移動編

以下のようなスクリプト内でディレクトリ移動を行う `change_directory.sh` を準備します。

```
$ cat change_directory.sh 
cd /tmp
```

`. スクリプトファイル名` の場合、同一プロセスのため、現在動作しているシェルのディレクトリが変わりました。

```
$ pwd
/home/vagrant

$ . change_directory.sh 

$ pwd
/tmp
```

`bash スクリプトファイル名` の場合、サブシェル（子プロセス）内でのみディレクトリ移動を行っているため、現在のシェルには影響を与えません。

```
$ pwd
/home/vagrant

$ bash change_directory.sh 

$ pwd
/home/vagrant
```

##### 環境変数上書き編

`~/.bash_profile` や `~/.profile` などの設定内容を書き換えた際に、なぜ  `.` （または `source` ）を利用するのかという理由についても、検証しておかないとですね。

現在のシェルで先ほど定義しておいた環境変数を `echo` で確認します。

```
$ echo $ENV_VARIABLE
This is environment variable!
```

定義済みの環境変数を上書きして表示するスクリプトを準備します。

```
$ cat overwrite_variables.sh 
ENV_VARIABLE='overwrite environment variable'
echo '$ENV_VARIABLE: '$ENV_VARIABLE
```

子プロセスで実行される `bash` の方から試してみましょう。子プロセス内では上書きに成功しているようです。

```
$ bash overwrite_variables.sh 
$ENV_VARIABLE: overwrite environment variable
```

現在のシェルで環境変数の値を確認するとスクリプト実行による影響はなく元の値を維持していることがわかります。

```
$ echo $ENV_VARIABLE
This is environment variable!
```

それでは `.` で試してみましょう。こちらも上書きに成功しているようです。

```
$ . overwrite_variables.sh 
$ENV_VARIABLE: overwrite environment variable
```

現在のシェルで環境変数の値を確認すると、同一プロセスIDで動作しているため、現在のシェルの環境でも上書きされていることがわかります。

```
$ echo $ENV_VARIABLE
overwrite environment variable
```

これらの動作結果から環境変数の値を `~/.bash_profile` などに記述して現在のシェルに反映させたい場合に、なぜ `.` （または `source` ）を利用する理由がわかりましね。
答えは `.` （または `source` ） は同一プロセスIDで実行されるので、スクリプト内の操作が現在のシェルに影響を与えるため、です。

---

### まとめ

シェルスクリプトの実行方法の違いについて色々と試した結果を表にまとめてみました。
これによりそれぞれの実行方法の特徴が見えてきたんじゃないでしょうか。

|  | . スクリプトファイル名 | bash スクリプトファイル名 |
|:-|:-|:-|
|同等のコマンドは|source スクリプトファイル名<br>※利用できないシェルがあるので注意が必要|./スクリプトファイル名|
|現在のシェルのプロセスIDと|同じプロセスIDで実行|異なるプロセスIDで実行<br>※子プロセスのIDが生成される|
|シェル変数|引き継がれる|引き継がれない|
|環境変数|引き継がれる|引き継がれる|
|スクリプト内の操作が現在のシェルに影響を|与える|与えない|

---

### おまけ

`.` （または `source` ）を「ファイルを読み込んで設定を反映するためのもの」のような理解をしているとこのような悲劇が起こりますのでご注意ください。。。

- [source ~/.zsh_historyは絶対に実行してはいけない(戒め](https://qiita.com/Seigenkousya/items/086e682bcbcbe0815126)

また、操作ミスによるこういった悲劇も避けたいものです。。。

- [sourceコマンドを誤って使ってしまいゾッとした話
](https://qiita.com/stilo/items/90f6c6e308c85a4a9460)

### 参考URL
- [bashの似てて紛らわしいもの '.' と 'source' について](https://takuya-1st.hatenablog.jp/entry/2017/01/07/111105)
- [shとsourceの違い](https://www.softel.co.jp/blogs/tech/archives/5971)
- [シェルスクリプトを実行する2種類の方法とその違い](https://www.smartbowwow.com/2020/04/2.html)
- [【 source 】コマンド／【 . 】コマンド――シェルの設定を即座に反映させる](https://www.atmarkit.co.jp/ait/articles/1712/21/news015.html)
- [シェルスクリプトの実行方法4つの違いメモ【Linux】](https://penpen-dev.com/blog/shellscript-tigai/)
- [【 source 】【.】 ファイルからコマンドを読み込んで現在のシェル環境で実行 【 Linuxコマンドまとめ 】](https://linuxfan.info/source)
- [sourceコマンドが.**rcをリフレッシュするコマンドじゃないと知る](https://qiita.com/masakuni-ito/items/8476d845fb4f69dc5225)
- [Linux — source コマンドは何をしているのか > 実は環境をリロードするためのものではない](https://qiita.com/YumaInaura/items/00437e6ab14d96adb71f)
- [UbuntuのMakefileでsourceコマンドを実行すると「source: コマンドが見つかりませんでした」と出る](https://qiita.com/yuta_vamdemic/items/ce8c86098d10971cdf57)
- [source ~/.bash_profile や source venv/bin/activate と何度も入力したことのある人へ](https://qiita.com/Mco7777/items/3e0a9ffe755088627d76)
- [source コマンドでシェルの設定を反映する](https://qiita.com/n-shigeki/items/e45fcbe0bd5f880aa57f)
- [Crontab で source ~/.bashrc や bash -l を使うと死を招く](https://qiita.com/tonluqclml/items/380a47c23a852b0d07b7)
- [source(.)の使い方](https://qiita.com/Pirlo/items/056bdc1022018972500a)
- ['. test.sh'と'sh test.sh'と'./test.sh'の違い](https://qiita.com/macoril/items/d86883933f54b582f353)