はじめに
世間一般では「シバンを書くべき」で終わっている話だと思うのですが、気になる先人の議論のまとめがあって、でもこのまとめを読んでもタイトルとずれた話ばかりで、さっぱりわからんで放置していたのですが、いい加減ちゃんと調べることにしました。(私のように)あのまとめを読んで混乱している人のための記事です。単にシェルスクリプトにシバンが必要なのか必要ないのかわからない人もどうぞ
おまけ この記事で参考にしたわけではありませんが、シバンについての良い記事
The #! magic, details about the shebang/hash-bang mechanism on various Unix flavours
結論
私の結論はタイトルの通り**「シバンを書いた方が可搬性が高いシェルスクリプトになる」**です。
理由 シバンがないスクリプトをインタラクティブシェルから実行すると、エラーになったり sh
以外のシェルで実行されることがあるから
シバンがないスクリプトを実行すると、シェルによって以下のいずれかの挙動になります。
-
/bin/sh
またはsh
で実行する - 現在使用しているインタラクティブシェルと同じシェルの別プロセスで実行する
シェルは POSIX シェルとは限りません - 実行ファイルではないというエラーになる
- その他(理論的には有り得る話だと思うが未確認)
世の中にあるシェルは POSIX シェル、Bourne シェル、(t)csh だけではありません。それ以外の独自シェルがあることも忘れないようにしましょう。
検証
シバンなしの ps
とだけ書いた以下のスクリプトを、いろんなシェル上で ./script
で実行してみました。基本的に macOS 上で検証していますが Ubuntu 上でも同じ挙動をしているようです。
ps
下記の「実行コマンド」とは上記の ps
コマンドで表示されたコマンドです。
POSIX シェル
シェル | 実行コマンド | 特殊条件下(下記補足参照) |
---|---|---|
dash 0.5.11.4 | dash |
|
bash 5.1.4 | bash |
|
ksh 2020 | ksh |
|
mksh R59 | /bin/sh ./script |
|
zsh 5.8 | sh ./script |
|
yash 2.51 | sh - ./script |
非 POSIX シェル
シェル | 実行コマンド | 特殊条件下(下記補足参照) |
---|---|---|
tcsh 6.21.00 | /bin/sh ./script |
/bin/tcsh ./script |
fish 3.2.2 | エラー | /bin/sh ./script |
xonsh 0.9.27 | xonsh ./script |
|
elvish 0.15.0 | エラー | |
PowerShell 7.1.3 | エラー | |
Nushell 0.33.0 | sh -c ./script |
補足
- tcsh 6.21.00: ファイルの一行目が
#
で始まる場合(ただしシバンではないこと) - fish 3.2.2: ファイルの一行目が
:
で始まる場合(仕様があるのか要確認)
エラーメッセージ
fish 3.2.2
Failed to execute process './script'. Reason:
exec: Exec format error
The file './script' is marked as an executable
but could not be run by the operating system.
elvish 0.15.0
Exception: fork/exec ./script: exec format error
[tty 2], line 1: ./script
PowerShell (pwsh) 7.1.3
ResourceUnavailable: Program 'script' failed to run: Exec format errorAt line:1 char:1
+ ./script
+ ~~~~~~~~.
Ubuntu は未調査
なぜこのような違いがあるのか?
シェルの実装によって動作が決まるから(以下は推測であることに注意 気が向いたらちゃんと調べるかもしれません)
- シェル上で
./script
を実行する - 一部のシェルはファイルの一行目を読んで特殊な例外処理を行う
- tcsh はファイルの最初が
#
であれば tcsh で実行する(→終了) - fish はファイルの最初が
:
であれば sh で実行する(→終了)
- tcsh はファイルの最初が
-
execl 系のシステムコールを呼び出してスクリプトを実行する
-
execlp
/execvp
以外を使用している場合- 実行可能ファイル(バイナリ)の場合は実行する(→終了)
- シバンがあればそのプログラムで実行する(→終了)
- その他(実行可能でないファイル)の場合はエラーとなりシェルに制御が戻る
-
execlp
/execvp
を使用している場合- 実行可能ファイル(バイナリ)の場合は実行する(→終了)
- シバンがあればそのプログラムで実行する(→終了)
- その他の場合は
sh
で実行する(→終了)
-
- 一部のシェルは実行可能ファイルではないというエラーを出力(→終了)
- フォールバックとして
sh
または自分と同じシェルでスクリプトを実行する(→終了)
私の感想・・・中身が不明なファイルを sh
や自分と同じシェルで実行すると何が起きるかわからないのだからそんなことやめればいいのにと思う(歴史的な理由だろうけど)
execl 系システムコールの挙動の調査
実行可能ファイル(バイナリ)ではなくシバンもないファイルを execl 系システムコールに渡した時に sh
を呼び出すのは execlp
と execvp
だけであることの確認です。以下のようなコードでシバンなしの ps
とだけ書かれた ./script
を実行して確認しました。(調査は macOS 上)
#include <stdio.h>
#include <unistd.h>
int main()
{
execl("./script", "script", NULL); // Exec format error
//execle("./script", "script", NULL, NULL); // Exec format error
//execlp("./script", "script", NULL); // sh ./script
char *argv[] = { "script", NULL };
//execv("./script", argv); // Exec format error
//execve("./script", argv, NULL); // Exec format error
//execvp("./script", argv); // sh ./script
perror("script");
return -1;
}
なお system()
や popen()
は内部で execl()
を使用しますが "sh", "-c", command
を実行すると POSIX で規定されているようです。sh -c
でどのシステムコールを使うかはおそらく POSIX では規定されておらずシェルの実装依存だと思うのですが execlp
と execvp
を使っていると無限に再帰呼び出ししてしまう可能性が考えられるので execlp
と execvp
以外を使っていると思います。
「可搬性が高い」の定義
私は実際に多くの環境で動くものを「可搬性が高い」と定義しています。「POSIX に準拠すれば可搬性が高い」という現実には存在しない架空の世界の話には興味がありません。架空の世界用にソフトウェアを作っても現実には誰にもメリットがないからです。
- ❌ POSIX に準拠していれば、動く可能性が高いだろう ← ただの推測
- ⭕ 実際に動くこと確かめたら、動くと断定できる ← 事実の方が勝る
POSIX はガイドラインのたぐいとして参考程度に留めるのが吉です。以下は最近私が書いた記事です。私の POSIX に対しての考えを書いています。
「POSIX では ○ と定義されているから、○ に決まっている」という考えは必ずしも正しいわけではなく「POSIX では ○ と定義されているけど、現実はそうではない」場合があるという考えを持つべきです。POSIX を盲信するのではなく、現実にはどうなのか?をちゃんと考えましょう。
予想される反論
「Android ではシェルスクリプトにシバンがない方が可搬性が高い」
前提知識 Android の システムシェルのパスは /bin/sh
ではなく /system/bin/sh
前提として 100% の可搬性がある方法は存在しないので何かを切り捨てるしかありません。私は 汎用のシェルスクリプトを Android に持ってきて実行する人よりも POSIX シェル以外のシェルから実行する人の方が多いと考えているので、より多く人にとって可搬性があるのはシバンがある方だというのが私の結論です。もちろんこれは一般論であり Android との可搬性をもたせたシェルスクリプトを書きたい人が(非 POSIX シェルユーザーを切り捨てて)シバンがないスクリプトを書くのは自由です。
ちなみに POSIX 的には シェルスクリプトのインストール時にシバンを書き換えることを推奨しているようです。(参考)
Furthermore, on systems that support executable scripts (the "#!" construct), it is recommended that applications using executable scripts install them using getconf PATH to determine the shell pathname and update the "#!" script appropriately as it is being installed (for example, with sed). For example:
個人的には /bin/sh
→ /system/bin/sh
へのシンボリックリンクファイルを作成するのが一番手軽な気もしますが、権限不足などでそれができない場合もあるでしょう。
シバンを書かなくてもいい場合
2021-07-08 追記
例外としてシバンを書かなくて良いのが .bashrc
や .bash_profile
などです。これらはシェルから新しいプログラムを実行しているのではなく現在のシェルに設定ファイルとして読み込むものであるため、シバンを書かなくていいし実行権限も付ける必要はありません。(書いてもコメントとして無視されるので実害はありませんが)
先人の議論のまとめ
- POSIXにおけるShebangの解釈についてのシェルショッカーとの議論まとめ
- シェルスクリプトにシバン(#!/bin/sh)はないほうがいいという説
- What is shebang (#!/bin/sh) in POSIX shell script
以下はおまけで「先人の議論のまとめ」の中の話に対する私のコメントです。
#! の動作は未定義 (unspecified)
https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html より
The shell reads its input from a file (see sh), from the -c option or from the system() and popen() functions defined in the System Interfaces volume of POSIX.1-2017. If the first line of a file of shell commands starts with the characters "#!", the results are unspecified.
少なくとも現在の POSIX は移植性がある完璧な統一仕様を作ろうとしてるのではなく現実の実装を文書化してるだけです。どこでも同じ動きをすると言えないものは未定義と文書化するだけだし矛盾しない形でうまく文章化できなければ何も書かない(≒ 未定義)で終わりです。POSIX としてはシバンの動作は未定義と文書化されてるだけで、これを知ったところで実際のシェルスクリプトを作る際にはなんの役にも立ちません。
system()
と popen()
は sh -c
と同じ動作をすると POSIX で規定されてるのでシェルがスクリプトを実行する時の動作は、大きく sh ./script
と sh -c "./script"
の 2 パターンになると考えられます。
-
sh ./script
は指定したファイルをシェルスクリプトとして実行します。ファイル内容をシェルで直接実行するのでscript
のシバンは無視されます。 -
sh -c "./script"
は引数をシェルスクリプト文字列として実行します。シェルスクリプトから外部コマンド./script
を実行するのと同じなのでscript
のシバンに従います。
2 のケースで script
にシバンがある場合は、POSIX 的には未定義なので何が起きてもおかしくないですが、現実に有り得そうなのは「シバンで指定されたコマンドで実行される」または「コメント扱いされ無視される」のどちらかですが後者の可能性はまずないでしょう。2 のケースで script
にシバンがない場合は、シェルで ./script
を実行した時と同じ挙動だろうと思われます。つまり「現在のシェルと同じシェルで実行する」もしくは「sh
で実行される」のどちらかです。(POSIX シェルでの検証結果を参照。sh -c
で指定したスクリプトから実行する以上、現在のシェルは POSIX シェルであるとみなすことが出来きます。)
補足 大昔のシェルでは 1 のケースでシェル自身がシバンに従って別のコマンドを呼び出すことがあるらしい(?)ですが、私としては未確認です。少なくとも Solaris 10 の Bourne シェルはそのような動きをしませんでした。相当古いシェルの話なのでしょう。
まとめると、実際のシェルスクリプトを書く際に知っておくべきことは以下の 2 つだけです。
-
sh ./script
はscript
のシバンを無視する。 -
sh -c "./script"
はscript
のシバンに従う。ただしシバンがない場合は、現在の POSIX シェルまたはsh
で実行する。
ファイルの最初が # だった場合 (t)csh を呼び出す
macOS の tcsh ではシバンがないファイルかつファイルの最初が #
である場合は tcsh を呼び出し、それ以外は sh を呼び出すことを確認しました。
3BSD の sh ではファイルの最初が #
である場合は /bin/csh
を呼び出すことがソースコードから読み取れます。
シェルスクリプトにシバン(#!/bin/sh)はないほうがいいという説
ほとんどがシバンと関係ない話で埋め尽くされています・・・。PATH
の初期化とか getconf
とか BusyBox とかの話はシバンと関係ないので別記事にします。
POSIXでは,シバンがなければ,sh で起動されると明記されている。
sh で実行されると明記されているのは execlp
と execvp
だけで、例えばとある C 言語のプログラムが execlp
と execvp
以外を使ってシェルスクリプトを実行する場合にもシバンがないとエラーになります。
またこのまとめでは POSIX シェルと (t)csh だけしか検証が行われておらず、それ以外の独自シェルが考慮されていません。非 POSIX シェルを使っている場合は シバンがなければエラーになったり 非 POSIX シェルで実行されることがあります。POSIX シェル以外のシェルを考慮するならシバンを書いたほうが良いです。
このときは,POSIXでshが#!から始まるファイルを読み込んだときの挙動がunspecifiedだったので,シバンには可搬性はないという結論だった。
確かに POSIX では #!
で始まるファイルを読み込んだ時の挙動は unspecified となっていますが、この意味は「可搬性があると主張しない」ということです。可搬性があると主張してない以上 100% の可搬性はないのだろうと推測することはできますが、可搬性が低い または 可搬性がない(0%)という意味ではありません。そして現実にはシバンがある方が可搬性が高いです。
POSIX規格でも,シバンの解釈は実装依存。
今となってはただの古き慣習であり,ファイルの拡張子だけでも意思表明は十分な気がしています。
技術的に利点が見いだせません。
例えば Debian の which
コマンドはシェルスクリプト製ですが拡張子が必須だと which
コマンドをシェルスクリプトや任意のスクリプト言語で作ることが出来ません。もちろん他のコマンドも同様です。
togetter まとめ
togetter まとめは、つぶやきのまとめであって、議論のまとめではない。