ShellScript
C
Linux
AdventCalendar
UNIX

スクリプト言語 C —— shebangのないテキストファイルはいかに実行されるか

そのまま実行できるC言語のソースファイルは作成可能か

スクリプト言語とは何か、これの定義はいろいろですが、ソースファイルを「そのまま実行」できる、という手軽さは重要です。
例えば、下のようなC言語のソースファイルがあったとします。

$ file hello.c 
hello.c: C source, ASCII text
$ gcc -Wall ./hello.c 
$ ./a.out
Hello, script language C.

警告なしにきちんとコンパイルされるC言語として正しいコードです。
このソースファイルに実行属性を与えて実行したとして、

$ # スクリプト言語のようにソースファイルを実行ファイルにする
$ chmod +x hello.c
$ ./hello.c
Hello, script language C.

のように問題なく実行されれば、C言語もスクリプト言語と言えるかも知れません。
このようなファイルが作れるか考えてみます。

案 1: TCCをshebangで使う

スクリプト言語では、ソースファイルの1行目に #!/bin/sh#!/usr/bin/python などといった形で、そのソースファイルを実行するためのコマンドを指定します。
shebang」です。
これをC言語でも使ってみましょう。

TCCというコンパイラには -run というオプションがあり、これを使うとコンパイルから実行まで自動で行なってくれますが、これはshebang行に指定することも出来ます。

hello_tcc.c
#!/usr/bin/tcc -run
#include <stdio.h>
int main (int argc, char* argv[]) {
    printf("Hello, script language C.\n");
    return 0;
}

これで tcc コマンドがインストール済みの環境ならば直接実行できるようになります。

$ chmod +x hello_tcc.c
$ ./hello_tcc.c 
Hello, script language C.

しかし、これはC言語として正しいでしょうか。

$ gcc -Wall ./hello_tcc.c 
./hello_tcc.c:1:2: error: invalid preprocessing directive #!
 #!/usr/bin/tcc -run
  ^

やはり、shebang行がエラーになりますね。
TCC以外ではコンパイル出来ません。

案 2: 実行属性を付けるだけ

shebang行があるとC言語ではなくなってしまうので削除します。

hello_x.c(実行属性を付ける)
#include <stdio.h>
int main (int argc, char* argv[]) {
    printf("Hello, script language C.\n");
    return 0;
}

もはやただのテキストファイルなのですが、これが門前払いされるかというと、そうでもありません。

シェルから実行された場合

$ chmod +x hello_x.c
$ ./hello_x.c
./hello_x.c: 2: ./hello_x.c: Syntax error: "(" unexpected

構文エラーが出ていますね。
何がこのエラーを出しているのか、シェルの動きを確認します。

$ #(手元の環境はLinuxなので `strace` コマンドで)
$ strace -f sh -c './hello_x.c'
straceの出力から抜粋
[pid 20349] execve("./hello_x.c", ["./hello_x.c"], [/* 80 vars */]) = -1 ENOEXEC (Exec format error)
[pid 20349] execve("/bin/sh", ["/bin/sh", "./hello_x.c"], [/* 80 vars */]) = 0

関連箇所を2行抜き出しました。
execveはUnixのexec関数群の中で最も基本的な機能を提供するものです。
Linuxなどではこれがそのままシステムコールなので、strace に現われます。

シェルの動作 1: ./hello_x.c の実行 → 失敗

まず、最初に execve./hello_x.c を実行してエラーになっています。

  • ENOEXEC:
    実行ファイルが理解できない形式であるか、違うアーキテクチャーのものか、その他のフォーマットエラーにより実行ができなかった。

shebangが無いテキストファイルは、execve には実行できません。

シェルの動作 2: シェル自身の実行

./hello_x.c の実行に失敗した直後、./hello_x.c を引数にして自分自身(sh)を実行しています(この動作を実現する方法はシェルによって多少違います。状況によっては execve を使わないかも知れません)。
これは、シェルの仕様として定められている動作です。

If the execve() function fails due to an error equivalent to the [ENOEXEC] error, the shell shall execute a command equivalent to having a shell invoked with the command name as its first operand, with any remaining arguments passed to the new shell
—— Shell Command Language

訳: [ENOEXEC]エラーに相当するエラーにより execve() 関数が失敗した場合、シェルは最初のオペランドとしてコマンド名を指定してシェルを呼び出すのと同じコマンドを実行し、残った引数は新しいシェルに渡されます。

しかし、./hello_x.c はシェルスクリプトではありませんので構文エラーになり、前述のエラーが表示されたということです。

シェル以外のプログラムから実行された場合

shebangが無いファイルをシェルから実行すると、シェル自身によってシェルスクリプトとして実行されることが解りました。
では、他のプログラムから実行された場合はどうでしょうか。

$ chmod +x ./hello_x.c
$ env ./hello_x.c 
./hello_x.c: 2: ./hello_x.c: Syntax error: "(" unexpected

上は env コマンドの例ですが、シェルで実行した際と同じ結果になっています。
これは、exec関数群の一部がシェルと同じことをするからです。

ファイルのヘッダーが実行形式として認識できない場合 (このとき呼び出そうとした execve(2) はエラー ENOEXEC で失敗する)、これらの関数はそのファイルを最初の引き数としたシェル (/bin/sh) を実行する

シェルのように環境変数 PATH からコマンドを探すようなプログラムは、多くの場合これらの関数を使うか、最初からシェルに丸投げします。
よって、
「実行属性が付いているのにshebangが無いファイルは、確実では無いが大抵の場合はシェルに渡されてシェルスクリプトとして実行される」
と言ってよさそうです。

案 3: C言語であり、シェルスクリプトでもあるソースファイル

前の案ではC言語がそのままシェルスクリプトとして実行されエラーになっていたことが解りました。
あとは以下の条件を満すコードを考えるだけです。

  • シェルスクリプトとして実行すると、自分をCコンパイラでコンパイルして実行する
  • C言語として解釈される際は、シェルスクリプト部分は無効になる

C言語のプリプロセッサ指令は # で始まります。
これがシェルスクリプトではコメントになることを利用します。

hello_c_and_sh.c
#undef SHELL_SCRIPT_CODE
#ifdef SHELL_SCRIPT_CODE
exec tcc -run "$0" "$@"
#endif

#include <stdio.h>
int main (int argc, char* argv[]) {
    printf("Hello, script language C.\n");
    return 0;
}

シェルスクリプトとして実行されると、exec でTCCの実行に移りますので、以降のC言語のコードは実行されません。
C言語として解釈される際は、SHELL_SCRIPT_CODEundef しているので、シェルスクリプト部分は常に読み飛ばされます。
スクリプトとして実行できて、

$ chmod +x hello_c_and_sh.c
$ ./hello_c_and_sh.c 
Hello, script language C.

普通にコンパイル可能なC言語のソースファイルでもあります。

$ gcc -Wall ./hello_c_and_sh.c 
$ ./a.out 
Hello, script language C.

短く書けるのでTCCを利用しましたが、もう少し一般にありそうなコンパイラにも対応してみました。

#undef SHELL_SCRIPT_CODE
#ifdef SHELL_SCRIPT_CODE
command -v tcc >/dev/null 2>/dev/null && exec tcc -run "$0" "$@"

tmp_execute="$(mktemp /tmp/tmp_execute.XXXXXX)"
c99 -o "$tmp_execute" "$0"
"$tmp_execute" "$@"
exit_code="$?"
rm "$tmp_execute"
exit "$exit_code"
#endif

#include <stdio.h>
int main (int argc, char* argv[]) {
    printf("Hello, script language C.\n");
    return 0;
}

TCCが無かった場合、c99 によるコンパイル、実行、実行ファイルの削除、などを行ないます。
/tmp/ に作った実行ファイルを確実に削除したい場合は、もう少し工夫が必要かも知れませんが、とりあえず完成とします。

この記事のライセンス

クリエイティブ・コモンズ・ライセンス
この記事はCC BY-SA 4.0(クリエイティブ・コモンズ 表示 4.0 継承 国際 ライセンス)の元で公開します。