ソースコード直接実行のテクニック
この記事では、ターミナルからコマンド一発で (sh code.cpp
のように)ソースコードやファイルをコンパイルし、更には実行・表示できるようにするためのテクニックを紹介する。
概要
コンパイルの必要なファイルを書いたものの、いちいちコンパイルや確認にコマンドを叩いたりオプションを何度も指定するのは面倒ということは偶にある [0] 。 普通の人は素直に Make 等を使ったり、それ用のラッパー等を使ったり、エディタで提供されている機能やプラグインやシェル履歴でどうにかするものなのであろうが。 ものぐさな私は、ソースコードをターミナルから直接コンパイルし実行する方法を考えていた。
そして、以下の言語について良い方法を思い付いた。
本当は TeX 等にもこの方法を適用したかったのだが、残念ながらうまい手が思い付かなかった。 TeX 等の他のフォーマットについて良い方法をご存知であれば是非 Twitter とかで教えていただきたい。
利点
紹介の手法では、以下のような利点がある。
- C ライクなコメント文法を持つ多くの言語に適用可能
- 非プログラミング言語にも適用可能
- シェルスクリプトをそのまま埋め込み実行できる
- コンパイラへコマンドラインオプション等も指定可能
- コンパイラ以外の呼び出しも可能
- 直接実行時に引数を与えてそれを受け取ることも可能
コンパイラへコマンドラインオプションを渡せるというのは、特に C や 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;
}
$ ls po.cpp $ sh po.cpp po $ ls cpp-po po.cpp $
Rust
///bin/true<<//
/*
//
rustc "$0" -o rust-po && ./rust-po
exit
*/
fn main() {
println!("po");
}
$ ls po.rs $ sh po.rs po $ ls po.rs rust-po $
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"];
}
$ ls po.dot $ sh po.dot $ ls dot-po.png po.dot $
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"];
}
$ 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 $
dot
コマンドと optipng
コマンドが存在する場合)コマンドラインオプション
実行時に渡した引数を、埋め込んだスクリプトから参照できる。
///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;
}
$ 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;
}
$ 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'.
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
で退出だ。
シェルスクリプトにスクリプト以外の大きなデータを持たせる手法として、ファイルの途中で exec
や exit
を必ず呼ぶことで、それ以降のデータをシェルに無視させ、その場所に自由なデータを置くというのは、割と一般的なものである。
たとえばインストーラや自己展開アーカイブが、データ(往々にしてバイナリ)を自身の末尾に持っておくことで、単一ファイルでスクリプトとデータの2つの独立した情報を持っていたりする。
テキストであっても、 grub の設定ファイル /etc/grub.d/40_custom
等がこのテクニックを使っている。
ヒアドキュメントでも大きなデータを埋め込むことはできるが、その場合埋め込むデータにヒアドキュメントの終端記号だけを持つ行が含まれないことを保証する必要がある。 一方でシェルスクリプトを途中で終了する場合は、データに制限はない。
exit
以降
あとは、元言語からシェルスクリプトを無視するために開始していたブロックコメントを閉じるだけだ。 コメントを閉じれば、あとはシェルの絡まない純粋なソースコードの領域である。