ソースコード直接実行のテクニック

この記事では、ターミナルからコマンド一発で (sh code.cpp のように)ソースコードやファイルをコンパイルし、更には実行・表示できるようにするためのテクニックを紹介する。

概要

コンパイルの必要なファイルを書いたものの、いちいちコンパイルや確認にコマンドを叩いたりオプションを何度も指定するのは面倒ということは偶にある [0] 。 普通の人は素直に Make 等を使ったり、それ用のラッパー等を使ったり、エディタで提供されている機能やプラグインやシェル履歴でどうにかするものなのであろうが。 ものぐさな私は、ソースコードをターミナルから直接コンパイルし実行する方法を考えていた。

そして、以下の言語について良い方法を思い付いた。

本当は TeX 等にもこの方法を適用したかったのだが、残念ながらうまい手が思い付かなかった。 TeX 等の他のフォーマットについて良い方法をご存知であれば是非 Twitter とかで教えていただきたい。

利点

紹介の手法では、以下のような利点がある。

  • C ライクなコメント文法を持つ多くの言語に適用可能
  • 非プログラミング言語にも適用可能
  • シェルスクリプトをそのまま埋め込み実行できる
    • コンパイラへコマンドラインオプション等も指定可能
    • コンパイラ以外の呼び出しも可能
  • 直接実行時に引数を与えてそれを受け取ることも可能

コンパイラへコマンドラインオプションを渡せるというのは、特に C や C++ において規格のバージョンを指定したりしたい場合において、非常に有用である。

実例

C 言語、 C++

            #if 0
gcc --std=c11 "$0" -o c-po && ./c-po
exit
#endif

#include <stdio.h>

int main(void) {
    puts("po");
    return 0;
}
          
C のソースコード (バージョン 1) po.c
            ///bin/true<<//
/*
//
gcc --std=c11 "$0" -o c-po && ./c-po
exit
*/

#include <stdio.h>

int main(void) {
    puts("po");
    return 0;
}
          
C のソースコード (バージョン 2) po.c
            $ ls
po.c
$ sh po.c
po
$ ls
c-po  po.c
$
          
C の実行例
C

これら2つのバージョンには、以下のような違いがあるため、場合によって使いわけるのが良い。

バージョン1では # をコード中で使うと、プリプロセッサがそれを解釈してしまい文法エラーになる。 単なるコメントには : "hello" のように null コマンドを使ってやれば良いが、 # という文字そのものが必要な場合は、 $(printf '\x23')echo -e '\x23' のようにするなどの工夫が必要だろう。

バージョン2では、 */ という文字列はコメント終端と見做されてしまうためそのまま使えない。 *''/ のように、間に空文字列を挟む等の工夫が必要になる。

C++ でも、やることは C と全く同じだ。

            #if 0
g++ --std=c++14 "$0" -o cpp-po && ./cpp-po
exit
#endif

#include <iostream>

int main(void) {
    std::cout << "po" << std::endl;
    return 0;
}
          
C++ のソースコード (バージョン 1) po.cpp
            $ ls
po.cpp
$ sh po.cpp
po
$ ls
cpp-po  po.cpp
$
          
C++ の実行例
C++

Rust

            ///bin/true<<//
/*
//
rustc "$0" -o rust-po && ./rust-po
exit
*/
fn main() {
    println!("po");
}
          
Rust のソースコード po.rs
            $ ls
po.rs
$ sh po.rs
po
$ ls
po.rs  rust-po
$
          
Rust の実行例
Rust

Rust は、コメントの構文が C と同じため、プリプロセッサを使わない方法がそのまま使える。 Rust における /// は doc comment になってしまうが、どうせ単体ソースで気軽に実行する用途では気にならないだろう。 どうしても駄目だというのであれば、 ////bin/true のように適宜スラッシュを増やしてどうにかすれば良いだろう。

Graphviz

            ///bin/true <<//
/*
//
dot -Tpng "$0" -o dot-po.png
exit
*/

digraph G {
    rankdir = LR;
    P -> O [label="po"];
}
          
Graphviz の dot ファイル po.dot
            $ ls
po.dot
$ sh po.dot
$ ls
dot-po.png  po.dot
$
          
Graphviz の実行例
Graphviz

Tips

複雑な処理

シェルスクリプトの如く任意のコマンド(ただし親のソースのコメント解除や文法エラーを引き起こす文字を除く)を置けるので、たとえば以下のようなことができる。

  • 実行・コンパイル前に cppcheck など lint ツールを適用する
  • 二度コンパイルして静的ライブラリと動的ライブラリ両方を生成する
  • 実行後、テンポラリファイルや実行バイナリを削除する
  • 実行に成功したら git commit する

たとえば、 graphviz の dot ファイルをコンパイルしつつ、 dot コマンドが存在しない場合にはメッセージを表示し、また optipng が存在していれば PNG ファイルを最適化するようなコードは、以下のようになる。

            ///bin/true <<//
/*
//
OUT_STEM="dot-po"
OUT_PNG="${OUT_STEM}.png"
if type dot >/dev/null ; then
    if dot -Tpng "$0" -o "${OUT_PNG}" ; then
        echo 'successfully built.'
        type optipng >/dev/null && optipng "${OUT_PNG}"
    else
        echo 'compile failed.' >&2
    fi
else
    echo '`dot` command not found. Graphviz is required.' >&2
fi
exit
*/

digraph G {
    rankdir = LR;
    P -> O [label="po"];
}
          
Graphviz の dot ファイル po.dot
            $ ls
po.dot
$ sh po.dot
successfully built.
** Processing: dot-po.png
221x59 pixels, 4x8 bits/pixel, RGB+alpha
Reducing image to 8 bits/pixel, 253 colors (1 transparent) in palette
Input IDAT size = 5199 bytes
Input file size = 5274 bytes

Trying:
  zc = 9  zm = 8  zs = 0  f = 0         IDAT size = 1948

Selecting parameters:
  zc = 9  zm = 8  zs = 0  f = 0         IDAT size = 1948

Output IDAT size = 1948 bytes (3251 bytes decrease)
Output file size = 2802 bytes (2472 bytes = 46.87% decrease)

$ ls
dot-po.png  po.dot
$
          
Graphviz の実行例 (dot コマンドと optipng コマンドが存在する場合)
Graphviz

コマンドラインオプション

実行時に渡した引数を、埋め込んだスクリプトから参照できる。

            ///bin/true<<//
/*
//
: 'Default output file is `./c-po`'.
OUT="${1:-"./c-po"}"
gcc --std=c11 "$0" -o "${OUT}" && "./${OUT}"
exit
*/

#include <stdio.h>

int main(void) {
    puts("po");
    return 0;
}
          
出力ファイルパスを指定可能にした C ソース po.c
            $ ls
po.c
$ sh po.c hoge
po
$ ls
hoge  po.c
$ sh po.c
po
$ ls
c-po  hoge  po.c
$
          
実行例
出力ファイルパスを指定可能にした例
            ///bin/true<<//
/*
//
MSG="${1:-"This is a default message."}"
gcc --std=c11 "$0" -DMSG="\"${MSG}\"" -o msg \
    && ./msg
exit
*/

#ifndef MSG
#   define MSG "This is a default message."
#endif
#include <stdio.h>

int main(void) {
    printf("%s\n", MSG);
    return 0;
}
          
マクロを指定可能にした C ソース msg.c
            $ sh msg.c
This is a default message.
$ sh msg.c "Hello, world"
Hello, world
$ sh msg.c 'こんちわ\nせかい'
こんちわ
せかい
$
          
実行例
マクロを指定可能にした例

解説

概要

C のバージョン1は単にプリプロセッサの記号とシェルスクリプトのコメント記号が同じであることを利用するだけなので言うまでもない。

C のバージョン2や他の各言語での方法は、図解すると以下のようになる。

        SHELL                                                     Source lang
shell block comment start -> ,---- ///bin/true<<//       <- Source line comment
  (ignored by shell)         |     /*              ----. <- Source block comment start
shell block comment end   -> `---- //                  |
shell commands            ->       command             |
shell commands            ->       command             |
shell exit                ->       exit                |
  (ignored by shell)               */              ----' <- Source block comment end

シェルコメント開始→ソースコメント開始→シェルコメント終了の時点で、それ以下の行はシェルのみが解釈することになる。 そこにはソースのコメント終端以外は自由に書けるので、コンパイルするなり最適化するなりしてしまう。 シェルでやることが全て済めば、 exit してしまえばそれ以下はシェルが解釈しなくなるので、ソースのコメントを閉じればおしまいである。

この方法は、上で例示した C 、 C++ 、 Rust 、 Graphviz (dot) のみに限らず、 //(或いは /)を行コメントとして解釈し、他にブロックコメント構文を持つような全ての言語に適用可能である、非常に汎用的な方法だ。

細部

1行目: ///bin/true<<//

/bin/true は、戻り値 0 を返すだけの単純かつ軽量なコマンド [1] であり、標準入力を無視し、何も出力せず終了する。 この「標準入力の無視」が重要で、ここにソースコードの中でシェルが無視すべき部分(ブロックコメント開始)が流れ込めば良いわけである。

この流し込みを、ヒアドキュメントで行う。 コマンドラインにて <<FOOBAR のようにすると、最初の FOOBAR という行までの内容が改行含めそのままテキストとして標準入力へ流されるというのが、ヒアドキュメントの機能である。 ヒアドキュメント終了の記号もまた、ソースコードの言語では無視されてほしいため、行コメントの記号(ここでは //)をそのまま使う。 ヒアドキュメントを無視させることでブロックコメント代わりに使う手法は、シェルスクリプトに限らず割と一般的である [2]

/bin/true を、スラッシュ2つでなく3つにして ///bin/true のように指定するのにも、理由がある。

E10) Why does cd // leave $PWD as //?

POSIX.2, in its description of cd, says that three or more leading slashes may be replaced with a single slash when canonicalizing the current working directory.

This is, I presume, for historical compatibility. Certain versions of Unix, and early network file systems, used paths of the form //hostname/path to access 'path' on server 'hostname'.

Bash FAQ, version 4.14, for Bash version 4.4, 2017-04-02

linux - what is path //, how is it different from / - Stack Overflow で紹介されているこの説明がすべてである。 three or more leading slashes may be replaced with a single slash とのことなので、まあ2つ並べるよりは3つ以上並べた方が安心だろうという、それだけのことだ。

とはいえ、これは昔の bash (今どうなのかは知らない)のカレントディレクトリの扱いについての記述であり、シェルスクリプト一般についてそのまま適用できるかは知らない。 そもそも互換性のためだろうとも書いてあるため、現代においてスラッシュは幾つあろうが関係ないのかもしれない。

2行目、3行目: ブロックコメント開始とヒアドキュメント終了

上で解説した図や流れのとおりである。 シェルがヒアドキュメントとして解釈をやめている隙に元言語でのブロックコメントを開始し、次の行でヒアドキュメントを閉じる。 この閉じは、元言語ではコメントとして無視される。

4行目以降: シェルスクリプト

4行目から exit までは、シェル (sh) が解釈するため、そのままシェルスクリプトである。 コマンドをいくら呼ぼうが、制御構造を使おうが、ネットワーク通信をしようが、好き放題できる。 そして用が済んだら exit で退出だ。

シェルスクリプトにスクリプト以外の大きなデータを持たせる手法として、ファイルの途中で execexit を必ず呼ぶことで、それ以降のデータをシェルに無視させ、その場所に自由なデータを置くというのは、割と一般的なものである。 たとえばインストーラや自己展開アーカイブが、データ(往々にしてバイナリ)を自身の末尾に持っておくことで、単一ファイルでスクリプトとデータの2つの独立した情報を持っていたりする。 テキストであっても、 grub の設定ファイル /etc/grub.d/40_custom 等がこのテクニックを使っている。

ヒアドキュメントでも大きなデータを埋め込むことはできるが、その場合埋め込むデータにヒアドキュメントの終端記号だけを持つ行が含まれないことを保証する必要がある。 一方でシェルスクリプトを途中で終了する場合は、データに制限はない。

exit 以降

あとは、元言語からシェルスクリプトを無視するために開始していたブロックコメントを閉じるだけだ。 コメントを閉じれば、あとはシェルの絡まない純粋なソースコードの領域である。