Running External Programs
ジュリアはシェル、Perl、Rubyからコマンドのバックティック表記を借用しています。しかし、ジュリアでは次のように書きます。
julia> `echo hello`
`echo hello`
いくつかの点で、さまざまなシェル、Perl、またはRubyでの動作と異なります:
- バックティックは、コマンドを即座に実行するのではなく、コマンドを表す
Cmd
オブジェクトを作成します。このオブジェクトを使用して、他のコマンドとパイプで接続することができます。run
それを、そしてread
またはwrite
それに接続します。 - コマンドが実行されると、Juliaは特にその出力をキャプチャするように設定しない限り、出力をキャプチャしません。デフォルトでは、コマンドの出力は
stdout
に送られます。これはlibc
のsystem
呼び出しを使用した場合と同様です。 - コマンドは決してシェルで実行されません。代わりに、Juliaはコマンド構文を直接解析し、シェルが行うように変数を適切に補間し、単語で分割します。コマンドは
julia
の即時子プロセスとして実行され、fork
およびexec
呼び出しを使用します。
以下は、LinuxやMacOSのようなPosix環境を前提としています。Windowsでは、echo
やdir
などの多くの類似コマンドは外部プログラムではなく、代わりにシェルcmd.exe
自体に組み込まれています。これらのコマンドを実行する1つのオプションは、cmd.exe
を呼び出すことです。例えば、cmd /C echo hello
のように実行します。あるいは、CygwinのようなPosix環境内でJuliaを実行することもできます。
外部プログラムを実行する簡単な例は次のとおりです:
julia> mycommand = `echo hello`
`echo hello`
julia> typeof(mycommand)
Cmd
julia> run(mycommand);
hello
hello
は、echo
コマンドの出力であり、stdout
に送信されます。外部コマンドが正常に実行されない場合、runメソッドはProcessFailedException
をスローします。
外部コマンドの出力を読みたい場合は、read
または readchomp
を代わりに使用できます:
julia> read(`echo hello`, String)
"hello\n"
julia> readchomp(`echo hello`)
"hello"
より一般的には、open
を使用して、外部コマンドから読み取ったり、書き込んだりすることができます。
julia> open(`less`, "w", stdout) do io
for i = 1:3
println(io, i)
end
end
1
2
3
コマンドのプログラム名と個々の引数は、コマンドが文字列の配列のようにアクセスされ、反復処理されることができます:
julia> collect(`echo "foo bar"`)
2-element Vector{String}:
"echo"
"foo bar"
julia> `echo "foo bar"`[2]
"foo bar"
Interpolation
ファイルの名前を変数 file
に格納し、その名前をコマンドの引数として使用したい場合、少し複雑なことをしたいとします。文字列リテラルのように $
を使って補間することができます(Strings を参照)。
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'`
ファイル名はどのように引用されるのか?ジュリアは 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 Vector{String}:
"/etc/passwd"
"/Volumes/External HD/data.csv"
julia> `grep foo $files`
`grep foo /etc/passwd '/Volumes/External HD/data.csv'`
配列をシェルワードの一部として補間すると、Juliaはシェルの{a,b,c}
引数生成をエミュレートします:
julia> names = ["foo","bar","baz"]
3-element Vector{String}:
"foo"
"bar"
"baz"
julia> `grep xylophone $names.txt`
`grep xylophone foo.txt bar.txt baz.txt`
さらに、複数の配列を同じ単語に補間すると、シェルのデカルト積生成の動作がエミュレートされます:
julia> names = ["foo","bar","baz"]
3-element Vector{String}:
"foo"
"bar"
"baz"
julia> exts = ["aux","log"]
2-element Vector{String}:
"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`
Quoting
必然的に、あまりにも単純ではないコマンドを書きたいと思うことがあり、引用符を使用する必要があります。以下は、シェルプロンプトでのPerlワンライナーの簡単な例です:
sh$ perl -le '$|=1; for (0..3) { print }'
0
1
2
3
Perlの式は、2つの理由からシングルクォートで囲む必要があります。1つ目は、スペースが式を複数のシェルワードに分割しないようにするため、2つ目は、$|
のようなPerlの変数の使用が補間を引き起こさないようにするためです。他のケースでは、補間が行われるようにダブルクォートを使用したい場合もあります。
sh$ first="A"
sh$ second="B"
sh$ perl -le '$|=1; print for @ARGV' "1: $first" "2: $second"
1: A
2: B
一般的に、Juliaのバックティック構文は慎重に設計されており、シェルコマンドをそのままバックティックに切り取って貼り付けることができ、正常に動作します:エスケープ、引用、および補間の動作はシェルと同じです。唯一の違いは、補間が統合されており、Juliaの単一の文字列値と複数の値のコンテナの概念を認識していることです。上記の2つの例を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はコマンドを実行する前に表示するため、簡単に安全にその解釈を確認することができ、何の損害も与えることなく行えます。
Pipelines
シェルのメタキャラクター、例えば |
、&
、および >
は、Juliaのバックティック内で引用符を付ける(またはエスケープする)必要があります:
julia> run(`echo hello '|' sort`);
hello | sort
julia> run(`echo hello \| sort`);
hello | sort
この式は、echo
コマンドを3つの単語を引数として呼び出します:hello
、|
、およびsort
。結果として、1行が印刷されます:hello | sort
。では、パイプラインはどのように構築するのでしょうか?バックティック内で'|'
を使用する代わりに、pipeline
を使用します:
julia> run(pipeline(`echo hello`, `sort`));
hello
このコマンドは、echo
コマンドの出力を sort
コマンドにパイプします。もちろん、ソートする行が1つしかないので、これはあまり面白くありませんが、もっと興味深いことができることは確かです:
julia> run(pipeline(`cut -d: -f3 /etc/passwd`, `sort -n`, `tail -n5`))
210
211
212
213
214
これはUNIXシステム上で最も高い5つのユーザーIDを印刷します。cut
、sort
、およびtail
コマンドはすべて、現在のjulia
プロセスの即時の子プロセスとして生成され、間にシェルプロセスはありません。Julia自体が、通常シェルによって行われるパイプの設定やファイルディスクリプタの接続を行います。Juliaがこれを自分で行うため、より良い制御を保持し、シェルではできないことをいくつか実行できます。
ジュリアは複数のコマンドを並行して実行できます:
julia> run(`echo hello` & `echo world`);
world
hello
出力の順序は非決定的です。なぜなら、2つの echo
プロセスがほぼ同時に開始され、互いに共有する stdout
ディスクリプタへの最初の書き込みを競い合うからです。Juliaは、これら2つのプロセスの出力を別のプログラムにパイプすることを許可します:
julia> run(pipeline(`echo world` & `echo hello`, `sort`));
hello
world
UNIXのパイプに関して言えば、ここで起こっているのは、単一のUNIXパイプオブジェクトが作成され、両方のecho
プロセスによって書き込まれ、パイプのもう一方の端がsort
コマンドによって読み取られるということです。
IOリダイレクションは、キーワード引数 stdin
、stdout
、および stderr
を pipeline
関数に渡すことで実現できます:
pipeline(`do_work`, stdout=pipeline(`sort`, "out.txt"), stderr="errs.txt")
Avoiding Deadlock in Pipelines
パイプラインの両端に対して単一のプロセスから読み書きする際は、カーネルにすべてのデータをバッファリングさせないようにすることが重要です。
例えば、コマンドの出力をすべて読み取る場合は、read(out, String)
を呼び出し、wait(process)
は呼び出さないでください。前者はプロセスによって書き込まれたすべてのデータを積極的に消費しますが、後者はリーダーが接続されるのを待ちながらデータをカーネルのバッファに保存しようとします。
別の一般的な解決策は、パイプラインのリーダーとライダーを別々の Task
に分けることです:
writer = @async write(process, "data")
reader = @async do_compute(read(process, String))
wait(writer)
fetch(reader)
(一般的に、リーダーは別のタスクではなく、私たちはすぐにfetch
するので、そうなります。)
Complex Example
高水準プログラミング言語、ファーストクラスのコマンド抽象、およびプロセス間のパイプの自動設定の組み合わせは、非常に強力です。簡単に作成できる複雑なパイプラインの感覚を与えるために、ここにいくつかのより洗練された例を示します。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
これは、1つのプロデューサーが2つの同時コンシューマーにデータを供給する古典的な例です。1つの perl
プロセスが0から5までの数字を含む行を生成し、2つの並行プロセスがその出力を消費します。1つは行の前に「A」を付け、もう1つは「B」を付けます。最初の行をどのコンシューマーが取得するかは非決定的ですが、一度そのレースに勝つと、行は1つのプロセスともう1つのプロセスによって交互に消費されます。(Perlで $|=1
を設定すると、各print文が stdout
ハンドルをフラッシュするため、この例が機能するために必要です。そうでなければ、すべての出力がバッファリングされ、一度にパイプに印刷され、1つのコンシューマープロセスによってのみ読み取られます。)
ここにさらに複雑なマルチステージのプロデューサー-コンシューマーの例があります:
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
この例は前の例と似ていますが、消費者の段階が2つあり、段階ごとにレイテンシが異なるため、飽和スループットを維持するために異なる数の並列ワーカーを使用します。
これらの例をすべて試して、どのように機能するかを確認することを強くお勧めします。
Cmd
Objects
バックティック構文は、タイプ Cmd
のオブジェクトを作成します。このようなオブジェクトは、既存の Cmd
または引数のリストから直接構築することもできます:
run(Cmd(`pwd`, dir=".."))
run(Cmd(["pwd"], detach=true, ignorestatus=true))
これにより、キーワード引数を介して Cmd
の実行環境のいくつかの側面を指定できます。たとえば、dir
キーワードは Cmd
の作業ディレクトリを制御します:
julia> run(Cmd(`pwd`, dir="/"));
/
env
キーワードを使用すると、実行環境変数を設定できます:
julia> run(Cmd(`sh -c "echo foo \$HOWLONG"`, env=("HOWLONG" => "ever!",)));
foo ever!
Cmd
の追加のキーワード引数を参照してください。 setenv
および addenv
コマンドは、それぞれ Cmd
実行環境変数を置き換えたり追加したりする別の手段を提供します。
julia> run(setenv(`sh -c "echo foo \$HOWLONG"`, ("HOWLONG" => "ever!",)));
foo ever!
julia> run(addenv(`sh -c "echo foo \$HOWLONG"`, "HOWLONG" => "ever!"));
foo ever!