シェルスクリプトのシバン(Shebang)にシェル以外のコマンドを記述したらどうなるのか?

2022-07-23Bash

はじめに

UNIX 上で動作するスクリプトは、1 行目に #! から始まる 1 行を記述します。
これによりスクリプトの実行系を認識させることができます。

シェルスクリプトの場合は #!/bin/sh#!/usr/bin/bash#!/usr/bin/env bash と記述します。

Ruby スクリプトの場合は #!/usr/bin/ruby#!/usr/bin/env ruby と記述することが多いでしょう。

この 1 行目の記述のことを「 シバン 」とか「 Shebang 」と呼びますが、今ひとつこのあたりの仕組が理解できてなかったため、シバンでいろいろ遊んでみることにしました。

検証環境

$ uname -moi
x86_64 MacBookPro16,1 Darwin

$ bash -version | head -n 1
GNU bash, バージョン 5.0.17(1)-release (x86_64-apple-darwin19.4.0)

(実験 1) #!/bin/rm と記述し実行してみる

突然ですが、 シバン の挙動を理解するために実験をしてみます。

一行目に #!/bin/rm とだけ記述されたシェルスクリプトを作成してみます。

# 作成
$ cat <<EOF > test.sh
#!/bin/rm
EOF

# 内容が正しいかを確認してみる
$ cat test.sh
#!/bin/rm

# 実行権限を付与
$ chmod +x test.sh

# カレントディレクトリを確認
$ ls -la
total 8
drwxr-xr-x 3 root root   96 Sep 27 13:46 .
drwxr-xr-x 1 root root 4096 Sep 27 13:34 ..
-rwx--x--x 1 root root   10 Sep 27 13:47 test.sh

作成したスクリプトを以下のように実行してみます。

$ ./test.sh

実行後、カレントディレクトリを確認してみると、 test.sh というスクリプトがなくなっています。

$ ls -la
total 4
drwxr-xr-x 2 root root   64 Sep 27 13:48 .
drwxr-xr-x 1 root root 4096 Sep 27 13:34 ..

これはなぜでしょう。

./test.sh を実行した結果、 /bin/rm ./test.sh が実行されたようです。
シバン付きのスクリプトを実行した場合、 シバンに書かれたコマンドが実行され、「スクリプト名」が引数として渡される のではないかと考えられます。

(実験 2) #!/bin/echo と記述し実行してみる

追加の実験をしてみます。

一行目に #!/bin/echo とだけ記述されたシェルスクリプトを作成してみます。

# 作成
$ cat <<EOF > test.sh
#!/bin/echo
EOF

# 内容が正しいかを確認してみる
$ cat test.sh
#!/bin/echo

# 実行権限を付与
$ chmod +x test.sh

作成したスクリプトを、いくつかのコマンドライン引数付きで実行してみます。

$ ./test.sh aa bb ccc
./test.sh aa bb ccc

ここから、以下のようなコマンドが実行されたようです。

$ /bin/echo ./test.sh aa bb ccc

改めて #!/bin/bash の意味を考える

改めて #!/bin/bash の意味を考えてみます。
例えば、 シバン付きの test.sh スクリプトを以下のように実行した場合を考えてみます。

$ ./test.sh aa bb ccc

これは、以下のようなコマンドが実行されることとなります。

/bin/bash ./test.sh aa bb ccc

このように整理できると、シェルスクリプトの中で ${0} という組み込み変数が 実行されたスクリプトファイルのパス を示している仕組みが理解できるようになります。

ちなみに御存知の通り、コマンドライン引数を表す変数の値は、 ${1}aa${2}bb${3}ccc となります。

(実験 3) #!/bin/echo first と記述し実行してみる

#!/usr/bin/env bash の意味についても考えてみましょう。

その前に実験です。
先程の echo コマンドをシバンとして設定したスクリプトを多少触ってみます。
一行目に #!/bin/echo first と記述されたシェルスクリプトを作成してみます。

$ cat <<EOF > test.sh
#!/bin/echo first
EOF

$ chmod +x test.sh

以下のように引数を付与して実行してみます。

$ ./test.sh xx yy zz

実行結果は以下の通りとなります。

first ./test.sh xx yy zz

これはつまり、 /bin/echo first ./test.sh xx yy zz というコマンドが実行されたと解釈できます。

#!/usr/bin/env bash も同様です。

#!/usr/bin/env bash がシバンに設定された test.sh スクリプトを実行すると、 /usr/bin/env bash ./test.sh というコマンドが実行されることとなります。

補足: env コマンドとは?

env コマンドを通すと $PATH 環境変数の設定を使って bash コマンドを探します。

/bin/bash にコマンドが配置されていない環境でも動作しますし、 bash が複数インストールされている場合(たとえばバージョン違い)にも、最適なもの ( $PATH の設定を使って最初に見つかったもの ) を検索してくれます。

したがって、僕個人の意見としてシバンは #!/bin/env よりも #!/usr/bin/env bash をおすすめします。

(実験 4) #!./test.sh xx yy zz と記述し実行してみる

最後の実験です。シバンの引数を 2 つ以上指定した場合はどのような挙動になるでしょう。

シバンにシェルスクリプトを指定し実行することで、この挙動をより深く理解することができます。

test.sh

$ cat <<'EOF' > test.sh
#!/bin/bash

echo "\$0 = $0"
echo "\$1 = $1"
echo "\$2 = $2"
echo "\$3 = $3"
EOF

$ chmod +x test.sh

作成した test.sh スクリプトに引数を付与して実行してみます。
引数が表示されるはずです。

$ ./test.sh xx yy zz
$0 = ./test.sh
$1 = xx
$2 = yy
$3 = zz

test.sh をシバンに指定した hoge.sh を用意します。

$ cat <<'EOF' > hoge.sh
#!./test.sh xx yy zz
EOF

$ chmod +x hoge.sh

作成した hoge.sh スクリプトに引数を付与して実行してみます。
すると面白いことがわかります。

$ ./hoge.sh 1 2 3
$0 = ./test.sh
$1 = xx yy zz
$2 = ./hoge.sh
$3 = 1

結果から、実行されたコマンドは以下のようなものだったことがわかります。
ポイントは "xx yy zz" の部分です。
シバンに指定された引数はダブルクォートで囲まれたように 1 つの引数として扱われています。

./test.sh "xx yy zz" ./hoge.sh 1 2 3

シバンに 2 つ以上の引数を指定する場合には注意が必要そうです。

ひとこと

多少はシバンの仕組みが整理できたでしょうか。

2022-07-23Bash