LoginSignup
1
2

More than 1 year has passed since last update.

シバン (shebang) を書いた方が可搬性が高いシェルスクリプトになる

Last updated at Posted at 2021-07-05

はじめに

世間一般では「シバンを書くべき」で終わっている話だと思うのですが、気になる先人の議論のまとめがあって、でもこのまとめを読んでもタイトルとずれた話ばかりで、さっぱりわからんで放置していたのですが、いい加減ちゃんと調べることにしました。(私のように)あのまとめを読んで混乱している人のための記事です。単にシェルスクリプトにシバンが必要なのか必要ないのかわからない人もどうぞ

おまけ この記事で参考にしたわけではありませんが、シバンについての良い記事
The #! magic, details about the shebang/hash-bang mechanism on various Unix flavours

結論

私の結論はタイトルの通り「シバンを書いた方が可搬性が高いシェルスクリプトになる」です。

理由 シバンがないスクリプトをインタラクティブシェルから実行すると、エラーになったり sh 以外のシェルで実行されることがあるから

シバンがないスクリプトを実行すると、シェルによって以下のいずれかの挙動になります。

  1. /bin/sh または sh で実行する
  2. 現在使用しているインタラクティブシェルと同じシェルの別プロセスで実行する
    シェルは POSIX シェルとは限りません
  3. 実行ファイルではないというエラーになる
  4. その他(理論的には有り得る話だと思うが未確認)

世の中にあるシェルは POSIX シェル、Bourne シェル、(t)csh だけではありません。それ以外の独自シェルがあることも忘れないようにしましょう。

検証

シバンなしの ps とだけ書いた以下のスクリプトを、いろんなシェル上で ./script で実行してみました。基本的に macOS 上で検証していますが Ubuntu 上でも同じ挙動をしているようです。

script
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 は未調査

なぜこのような違いがあるのか?

シェルの実装によって動作が決まるから(以下は推測であることに注意 気が向いたらちゃんと調べるかもしれません)

  1. シェル上で ./script を実行する
  2. 一部のシェルはファイルの一行目を読んで特殊な例外処理を行う
    1. tcsh はファイルの最初が # であれば tcsh で実行する(→終了)
    2. fish はファイルの最初が : であれば sh で実行する(→終了)
  3. execl 系のシステムコールを呼び出してスクリプトを実行する
    1. execlp / execvp 以外を使用している場合
      1. 実行可能ファイル(バイナリ)の場合は実行する(→終了)
      2. シバンがあればそのプログラムで実行する(→終了)
      3. その他(実行可能でないファイル)の場合はエラーとなりシェルに制御が戻る
    2. execlp / execvp を使用している場合
      1. 実行可能ファイル(バイナリ)の場合は実行する(→終了)
      2. シバンがあればそのプログラムで実行する(→終了)
      3. その他の場合は sh で実行する(→終了)
  4. 一部のシェルは実行可能ファイルではないというエラーを出力(→終了)
  5. フォールバックとして sh または自分と同じシェルでスクリプトを実行する(→終了)

私の感想・・・中身が不明なファイルを sh や自分と同じシェルで実行すると何が起きるかわからないのだからそんなことやめればいいのにと思う(歴史的な理由だろうけど)

execl 系システムコールの挙動の調査

実行可能ファイル(バイナリ)ではなくシバンもないファイルを execl 系システムコールに渡した時に sh を呼び出すのは execlpexecvp だけであることの確認です。以下のようなコードでシバンなしの 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 では規定されておらずシェルの実装依存だと思うのですが execlpexecvp を使っていると無限に再帰呼び出ししてしまう可能性が考えられるので execlpexecvp 以外を使っていると思います。

「可搬性が高い」の定義

私は実際に多くの環境で動くものを「可搬性が高い」と定義しています。「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 などです。これらはシェルから新しいプログラムを実行しているのではなく現在のシェルに設定ファイルとして読み込むものであるため、シバンを書かなくていいし実行権限も付ける必要はありません。(書いてもコメントとして無視されるので実害はありませんが)

先人の議論のまとめ

以下はおまけで「先人の議論のまとめ」の中の話に対する私のコメントです。

#! の動作は未定義 (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 ./scriptsh -c "./script" の 2 パターンになると考えられます。

  1. sh ./script は指定したファイルをシェルスクリプトとして実行します。ファイル内容をシェルで直接実行するので script のシバンは無視されます。
  2. sh -c "./script" は引数をシェルスクリプト文字列として実行します。シェルスクリプトから外部コマンド ./script を実行するのと同じなので script のシバンに従います。

2 のケースで script にシバンがある場合は、POSIX 的には未定義なので何が起きてもおかしくないですが、現実に有り得そうなのは「シバンで指定されたコマンドで実行される」または「コメント扱いされ無視される」のどちらかですが後者の可能性はまずないでしょう。2 のケースで script にシバンがない場合は、シェルで ./script を実行した時と同じ挙動だろうと思われます。つまり「現在のシェルと同じシェルで実行する」もしくは「sh で実行される」のどちらかです。(POSIX シェルでの検証結果を参照。sh -c で指定したスクリプトから実行する以上、現在のシェルは POSIX シェルであるとみなすことが出来きます。)

補足 大昔のシェルでは 1 のケースでシェル自身がシバンに従って別のコマンドを呼び出すことがあるらしい(?)ですが、私としては未確認です。少なくとも Solaris 10 の Bourne シェルはそのような動きをしませんでした。相当古いシェルの話なのでしょう。

まとめると、実際のシェルスクリプトを書く際に知っておくべきことは以下の 2 つだけです。

  1. sh ./scriptscript のシバンを無視する。
  2. sh -c "./script"script のシバンに従う。ただしシバンがない場合は、現在の POSIX シェルまたは sh で実行する。

ファイルの最初が # だった場合 (t)csh を呼び出す

macOS の tcsh ではシバンがないファイルかつファイルの最初が # である場合は tcsh を呼び出し、それ以外は sh を呼び出すことを確認しました。

3BSD の sh ではファイルの最初が # である場合は /bin/csh を呼び出すことがソースコードから読み取れます。

シェルスクリプトにシバン(#!/bin/sh)はないほうがいいという説

ほとんどがシバンと関係ない話で埋め尽くされています・・・。PATH の初期化とか getconf とか BusyBox とかの話はシバンと関係ないので別記事にします。

POSIXでは,シバンがなければ,sh で起動されると明記されている。

sh で実行されると明記されているのは execlpexecvp だけで、例えばとある C 言語のプログラムが execlpexecvp 以外を使ってシェルスクリプトを実行する場合にもシバンがないとエラーになります。

またこのまとめでは POSIX シェルと (t)csh だけしか検証が行われておらず、それ以外の独自シェルが考慮されていません。非 POSIX シェルを使っている場合は シバンがなければエラーになったり 非 POSIX シェルで実行されることがあります。POSIX シェル以外のシェルを考慮するならシバンを書いたほうが良いです。

このときは,POSIXでshが#!から始まるファイルを読み込んだときの挙動がunspecifiedだったので,シバンには可搬性はないという結論だった。

確かに POSIX では #! で始まるファイルを読み込んだ時の挙動は unspecified となっていますが、この意味は「可搬性があると主張しない」ということです。可搬性があると主張してない以上 100% の可搬性はないのだろうと推測することはできますが、可搬性が低い または 可搬性がない(0%)という意味ではありません。そして現実にはシバンがある方が可搬性が高いです。

POSIX規格でも,シバンの解釈は実装依存。
今となってはただの古き慣習であり,ファイルの拡張子だけでも意思表明は十分な気がしています。
技術的に利点が見いだせません。

例えば Debian の which コマンドはシェルスクリプト製ですが拡張子が必須だと which コマンドをシェルスクリプトや任意のスクリプト言語で作ることが出来ません。もちろん他のコマンドも同様です。

togetter まとめ

togetter まとめは、つぶやきのまとめであって、議論のまとめではない。

1
2
0

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
1
2