外部プログラムの実行
Julia はシェル・Perl・Ruby と同様のバックティックを使ったコマンドの記法を採用します:
julia> `echo hello`
`echo hello`
ただしこれはシェル・Perl・Ruby といくつかの点で異なります:
- コマンドはすぐに実行されず、バックティックはコマンドを表す
Cmd
オブジェクトを作成します。Cmd
オブジェクトはrun
で実行したりpipeline
を使ってパイプで繫いだりできるほか、read
とwrite
でコマンドに対して読み書きを行うこともできます。 - コマンドを実行するとき、特別な設定をしなければ Julia はコマンドの出力をキャプチャしません。デフォルトだとコマンドの出力は
stdout
に書き込まれます (libc
のsystem
を使ったときと同様です)。 - コマンドの実行でシェルは使われません。Julia はコマンドの構文を直接パースし、適切に変数を補間し、シェルと同様のクオート記法を使って単語を区切ります。コマンドは
fork
とexec
を使ってjulia
の直接の子プロセスとして実行されます。
外部プログラムを実行する簡単な例を示します:
julia> mycommand = `echo hello`
`echo hello`
julia> typeof(mycommand)
Cmd
julia> run(mycommand);
hello
最後の hello
は echo
コマンドからの出力が stdout
に送られたものです。run
メソッド自体は nothing
を返し、実行した外部コマンドが正常に終了しなければ ErrorException
を送出します。
実行する外部コマンドの出力を読むには read
を使ってください:
julia> a = read(`echo hello`, String)
"hello\n"
julia> chomp(a) == "hello"
true
より一般的には、open
を使えば外部コマンドの入力と出力を設定できます:
julia> open(`less`, "w", stdout) do io
for i = 1:3
println(io, i)
end
end
1
2
3
Cmd
オブジェクトに含まれるプログラム名と引数に対しては文字列の配列であるかのようにアクセスや反復が可能です:
julia> collect(`echo "foo bar"`)
2-element Array{String,1}:
"echo"
"foo bar"
julia> `echo "foo bar"`[2]
"foo bar"
補間
少し複雑な処理を書いていて、変数 file
に入ったファイルの名前をコマンドの引数として使いたいとします。このような場合には $
を使えば文字列リテラルと同様に補間が可能です:
julia> file = "/etc/passwd"
"/etc/passwd"
julia> `sort $file`
`sort /etc/passwd`
シェルを通して外部プログラムを実行するときにありがちなミスが、シェルで特別な意味を持つ文字がファイル名に含まれていて予期せぬ動作を引き起こすというものです。例えばソートするファイルが /etc/passwd
ではなくて /Volumes/External HD/data.csv
だと、ファイル名に空白が含まれるのでコマンドが壊れるかもしれません。試してみましょう:
julia> file = "/Volumes/External HD/data.csv"
"/Volumes/External HD/data.csv"
julia> `sort $file`
`sort '/Volumes/External HD/data.csv'`
ファイル名がクオートされているのはなぜでしょうか? Julia は file
が単一の引数として使われていることを理解し、この単語を自動的にクオートしたのです。付け加えておくと、この説明は正確ではありません: シェルは file
の値を解釈しないので、実際にはクオートは必要となりません。クオートはユーザーに分かりやすいように追加されると言えます。シェルの単語の一部として補間を行ってもクオートは行われます:
julia> path = "/Volumes/External HD"
"/Volumes/External HD"
julia> name = "data"
"data"
julia> ext = "csv"
"csv"
julia> `sort $path/$name.$ext`
`sort '/Volumes/External HD/data.csv'`
変数 path
に含まれる空白が適切にエスケープされていることが分かります。しかし本当に複数の単語を補間したい場合にはどうすればよいのでしょうか? この場合には、配列 (もしくは適当な反復可能コンテナ) を使ってください:
julia> files = ["/etc/passwd","/Volumes/External HD/data.csv"]
2-element Array{String,1}:
"/etc/passwd"
"/Volumes/External HD/data.csv"
julia> `grep foo $files`
`grep foo /etc/passwd '/Volumes/External HD/data.csv'`
シェルの単語の一部を配列で補完すると、シェルの {a,b,c}
を使った引数生成に似た処理が行われます:
julia> names = ["foo","bar","baz"]
3-element Array{String,1}:
"foo"
"bar"
"baz"
julia> `grep xylophone $names.txt`
`grep xylophone foo.txt bar.txt baz.txt`
さらに、複数の配列を一つの単語に補完すると、シェルと同様に直積に関して補間が行われます:
julia> names = ["foo","bar","baz"]
3-element Array{String,1}:
"foo"
"bar"
"baz"
julia> exts = ["aux","log"]
2-element Array{String,1}:
"aux"
"log"
julia> `rm -f $names.$exts`
`rm -f foo.aux foo.log bar.aux bar.log baz.aux baz.log`
補間には配列リテラルも使えるので、このようなコマンドの生成は一時的な配列オブジェクトを変数に保存しなくても行えます:
julia> `rm -rf $["foo","bar","baz","qux"].$["aux","log","pdf"]`
`rm -rf foo.aux foo.log foo.pdf bar.aux bar.log bar.pdf baz.aux baz.log baz.pdf qux.aux qux.log qux.pdf`
クオート
必然的に、私たちは単純とは言えないコマンドを書くので、一重引用符によるクオートが必要になります。次に示すのはシェルのプロンプトに書かれた簡単な Perl ワンライナーです:
$ perl -le '$|=1; for (0..3) { print }'
0
1
2
3
最後の Perl 式がクオートされている理由は二つあります: スペースが式をシェルの単語として区切らないようにするため、そして $|
のような Perl 変数 (ええ、これは Perl の変数です) が補間されないようにするためです。また別の場合には、二重引用符を使って補間が起こるようにする場合もあります:
$ first="A"
$ second="B"
$ perl -le '$|=1; print for @ARGV' "1: $first" "2: $second"
1: A
2: B
一般的に言って、Julia のバックティックを使った構文はシェルのコマンドをそのままバックティックで囲めば上手く動くように注意深く設計されています: エスケープ・クオート・補間の振る舞いはシェルと同様です。唯一の違いは補間が Julia に組み込まれていること、そして単一の文字列値とは何か、複数の値を持つコンテナは何かを補間が理解する点です。上記の二つの例を Julia で試してみます:
julia> A = `perl -le '$|=1; for (0..3) { print }'`
`perl -le '$|=1; for (0..3) { print }'`
julia> run(A);
0
1
2
3
julia> first = "A"; second = "B";
julia> B = `perl -le 'print for @ARGV' "1: $first" "2: $second"`
`perl -le 'print for @ARGV' '1: A' '2: B'`
julia> run(B);
1: A
2: B
出力は同じであり、Julia の補間処理はシェルと同様に行われます。ただ、たいていのシェルでは文字列に含まれるスペースを使って補間文字列を区切るために曖昧さが生まれるのに対して、Julia はファーストクラスの反復可能オブジェクトによる補間をサポートします。シェルコマンドを Julia に移植するときは、まずそのまま貼り付けてみてください。Julia では実行するコマンドを確認できるので、システムに損害を与えることなく安全に補間結果を確認できます。
パイプライン
|
, &
, >
といったメタ文字は Julia のバックティックの中でクオートが必要です:
julia> run(`echo hello '|' sort`);
hello | sort
julia> run(`echo hello \| sort`);
hello | sort
この二つの式はどちらも echo
コマンドを hello
, |
, sort
という三つの引数で起動するので、表示されるのは hello | sort
という文字列です。ではパイプラインの構築はどうするのでしょうか? パイプを使うには、バックティックの中で '|'
を使うのではなく、pipeline
関数を使います:
julia> run(pipeline(`echo hello`, `sort`));
hello
こうすると echo
コマンドの出力が sort
コマンドの入力に繋がります。もちろん一行の文字列をソートするだけでは面白くありませんが、パイプラインを使えばもっと興味深い処理を行えます:
julia> run(pipeline(`cut -d: -f3 /etc/passwd`, `sort -n`, `tail -n5`))
210
211
212
213
214
このパイプラインは UNIX システムに含まれる ID を大きい順に五つ表示します。cut
, sort
, tail
は現在の julia
プロセスの直接の子として起動され、その間に介入するシェルプロセスはありません。通常はシェルが行うパイプの構築とファイル記述子の接続を、Julia 自身が行っているためです。Julia が自分でパイプの構築処理を行うので細かい制御が可能であり、シェルにできないことが行えます。
Julia は複数のコマンドを並列に実行できます:
julia> run(`echo hello` & `echo world`);
world
hello
このコマンドの出力順序は実行のたびに変化します。二つの echo
プロセスはほぼ同時に開始され、親プロセス julia
と共有する stdout
記述子に対して最初に書き込んだ方が最初に表示されるからです。Julia では複数のプロセスからの出力をパイプして一つのプログラムの入力にできます:
julia> run(pipeline(`echo world` & `echo hello`, `sort`));
hello
world
UNIX の言葉を使うと、このコマンドでは単一の UNIX パイプオブジェクトが作成され、その入力側に二つの echo
が書き込み、出力側から sort
が読み込むという処理が起こっています。
IO のリダイレクトは pipeline
関数にキーワード引数 stdin
, stdout
, stderr
を渡すことで行います:
pipeline(`do_work`, stdout=pipeline(`sort`, "out.txt"), stderr="errs.txt")
パイプラインではデッドロックを避ける
パイプラインの両端に単一のプロセスから書き込みと読み込みを行うときは、カーネルがデータをバッファしないようにすることが重要です。
例えばコマンドの出力を全て読み込むときは、wait(process)
ではなく read(out, String)
としてください。read(out, String)
はプロセスが書き込んだ全てのデータを積極的に消費しようとするのに対して、wait(process)
は読み込み側が接続するまでの間にデータがカーネルのバッファに移される可能性があります。
パイプラインの読み込み側と書き込み側を個別の Task
にするという解決法もあります:
writer = @async write(process, "data")
reader = @async do_compute(read(process, String))
wait(writer)
fetch(reader)
複雑な例
高水準言語、ファーストクラスのコマンドの抽象化、そしてプロセスをつなぐパイプの自動的な設定の組み合わせは非常に強力です。複雑なパイプラインが簡単に作れることを感じてもらうために、一つ洗練された例を示します。Perl のワンライナーが多く使われることを最初に謝っておきます:
julia> prefixer(prefix, sleep) =
`perl -nle '$|=1; print "'$prefix' ", $_; sleep '$sleep';'`;
julia> run(pipeline(`perl -le '$|=1; for(0..5){ print; sleep 1 }'`,
prefixer("A",2) & prefixer("B",2)));
B 0
A 1
B 2
A 3
B 4
A 5
これは一つの生産者が二つの消費者に並行してデータを送り付けるという古典的な例です: 一つの perl
プロセスが 0 から 5 までの自然数が書かれた行を生成し、二つの並列なプロセスがそれを消費します。消費者の一つは自然数の最初に "A"
を付けた文字列を出力し、もう一つは自然数の最初に "B"
を付けた文字列を出力します。最初の行がどちらの消費者のものになるかは実行するたびに変化しますが、最初の行以降は必ず二つのプロセスが互い違いに入力を処理します (Perl で $|=1
とすると、print
するたびに stdout
ハンドルがフラッシュされるようになります。この例で print
ごとのフラッシュは必須であり、行わないと出力がバッファされて一度にパイプへ書き込まれるので、一つの消費者プロセスがまとめて入力を読むことになります)。
複数のステージを持つさらに複雑な生産者-消費者の例を示します:
julia> run(pipeline(`perl -le '$|=1; for(0..5){ print; sleep 1 }'`,
prefixer("X",3) & prefixer("Y",3) & prefixer("Z",3),
prefixer("A",2) & prefixer("B",2)));
A X 0
B Y 1
A Z 2
B X 3
A Y 4
B Z 5
この例は一つ前の例と似ていますが、消費者が二つのステージを持つ点が異なります。二つのステージは異なるレイテンシを持つので、並列に動作するワーカーの数を変えてスループットを飽和させるようにしています。
こういった例を全て自分で試してどのように動作するのかを理解しておくことを強く推奨します。