外部プログラムの実行

Julia はシェル・Perl・Ruby と同様のバックティックを使ったコマンドの記法を採用します:

julia> `echo hello`
`echo hello`

ただしこれはシェル・Perl・Ruby といくつかの点で異なります:

外部プログラムを実行する簡単な例を示します:

julia> mycommand = `echo hello`
`echo hello`

julia> typeof(mycommand)
Cmd

julia> run(mycommand);
hello

最後の helloecho コマンドからの出力が 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

この例は一つ前の例と似ていますが、消費者が二つのステージを持つ点が異なります。二つのステージは異なるレイテンシを持つので、並列に動作するワーカーの数を変えてスループットを飽和させるようにしています。

こういった例を全て自分で試してどのように動作するのかを理解しておくことを強く推奨します。

広告