shebang (#!) 以下の内容(コンテンツ)を読み出す方法


はじめに

シェルスクリプトはもとより、lua などのシェルじゃないスクリプト言語なども、#! を使って

exec-by-shebang.lua
#!/usr/bin/env lua
print("HEELO")
print(1+2)

などと記述することができます。このように書かれたファイルに実行属性を与えて実行すれば、#! の直後に書かれたプログラムにより(この例では lua により)、2 行目以降に書かれた lua プログラムが実行されます。

ちなみに、#! のことを、shebang - シバンとかシェバンとか呼ぶそうです。

これは、一体どのように実現されているのでしょうか? 例えば、自作のプログラムで、

test
#!./my-great-program
parameter / my great program
command-for-my-great-program
  :

などという書き方を実現するためには、どのようにすれば良いのでしょうか?

ベテラン Unix プログラマの方からみたら、非常に基本的な事柄なのだろうと思いますが、検索してもそのものズバリの答えを見つけることができなかったので、ここに記しておきます(多分、私の調べ方が悪いのだと思いますが、シェルスクリプトの書きかたに関する情報が大量に出てきてしまって、答えにたどり着くまでに随分時間がかかってしまいました…)。

いきなり結論

#! で起動されると、呼び出したスクリプトファイルのファイル名が引数として渡されます。

つまり、shebang-test に、

sheebang-test
#!./read-shebang-contents
shebang contents string
alpha
beta

などと書かれていたとすると、このファイルから呼び出される read-shebang-contents プログラムは、コマンドライン的に書くと、

exec-by-cli
%./read-shebang-contents ./shebang-test

となります。つまり、第 1 引数にシェルスクリプトのファイル名である shebang-test を付けた状態で起動されます。

サンプルプログラム

検証するためのプログラムを以下に示します。単に、argv を表示し、かつ、argv[1] に渡されたファイルの中身を表示する簡単なプログラムです。

read-shebang-contents.c
#include <stdio.h>
#include <unistd.h>

int main(int argc,char *argv[]) {
    int i;
    for(i=0; i<argc; i++) {
        printf("argv[%d]=%s\n",i,argv[i]);
    }
    if(argc<2) {
        printf("NO SCRIPT FILES.\n");
        return 0;
    }

    char buf[1024];
    FILE *fp=fopen(argv[1],"r");
    puts("===shell script contents===");
    while(fgets(buf,sizeof(buf),fp)!=NULL) {
        fputs(buf,stdout);
    }
    puts("=== END ===");
    fclose(fp);

    return 0;
}

シェルスクリプト shebang-test を実行した結果は以下のとおりです。

resut-of-shebang-test
argv[0]=./read-shebang-contents
argv[1]=./shebang-test
===shell script contents===
#!./read-shebang-contents
shebang contents string
alpha
beta
=== END ===

まとめ

shebang により起動されるプログラムには、起動するスクリプトファイルのファイル名が引数として渡されます。ただそれだけ。非常に簡単です。

冒頭に挙げた lua のスクリプトも、引数として渡されたファイル名をたよりに、ファイルをオープンしているようです。lua 言語でのコメントは # ではありませんので、ファイルを読み込む時になにか特別な処理をしているのではないかと思われます(どうやら最初の行の #! だけは無視するように処理をしているっぽいですが…)。

余談

本件は、分かってしまえば何も難しいところはない仕組みですが、分かるまでに少し時間がかかってしまいました。

当初は、シェルがプログラムを起動する際に exec 系のシステムコールを使うから、オープンされているファイルディスクリプタが引き渡されるのではないのか!?などとも思ったりしておりました。Unix について、あまり詳しくないとこんな風に思ってしまうという例です(他山の石にしていただければ幸いです)。

最終的なヒントとなったのは、lua のソースコードでした。lua は冒頭で示したように、#! で起動するファイルにプログラムを書いておけば、それを読み込み実行してくれます。検索してもなかなか分からない状況でしたので、最終的には lua のソースコード(lua.c)を読み、printf を所々に挿入して、make macosx しつつ、その挙動を調べていきました。21 世紀になっても、検索能力が低いと、結局「ソースを読め!」ということなんだなぁ、としみじみ思いつつ、また、トホホな気分になった日曜日でした。

おわり。