25
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

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

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

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


今回の動作確認環境

  • Vagrant 2.2.14
  • CentOS 8

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

まず、 .source についてですが man bash 内の SHELL BUILTIN COMMANDS を見ると、以下のように同列に記載されていて、「現在のシェル環境でファイルを読み込んで実行する」と説明されています。このことから .source に違いはなく、同等のコマンドであると考えてよさそうです。( . および sourcealiascd などと同じく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/shbash へのシンボリックリンクになっているため 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/shdash へのシンボリックリンクになっていて 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 の記述が必要だということは以下の記事でまとめています。

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

つまり、 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 スクリプトファイル名
※利用できないシェルがあるので注意が必要
./スクリプトファイル名
現在のシェルのプロセスIDと 同じプロセスIDで実行 異なるプロセスIDで実行
※子プロセスのIDが生成される
シェル変数 引き継がれる 引き継がれない
環境変数 引き継がれる 引き継がれる
スクリプト内の操作が現在のシェルに影響を 与える 与えない

おまけ

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

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

参考URL

25
18
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?