Julia その6 外部プロセス呼び出し

## 外部プロセスの呼び出し
Juliaの外部プロセス呼び出しには、`Cmd`という型のオブジェクトを用いる。が、この型は直接作ることはできず、リテラルとしてバッククオート`` ` `` で命令列をくくる形でしか作れないようだ。

```Julia
run(`ls -al`)
```
実行結果はttyに出力される。

### 空白を含む引数
この手の呼び出しでよく問題になるのは、ディレクトリ名にスペースが入っていたりする場合の回避方法。普通は引数を分離するために、配列を引数とするコンストラクタを用意するものだが、Juliaにはない。その代わり、なんとクォートを自前で解析して引数を分割してくれる。なぜだ。

```Julia
julia> `a b c`
`a b c`
julia> `a 'b c'`
`a 'b c'`
julia> `a "b c"`
`a 'b c'`
```
シングルクォートでもダブルクォートでも同じように解析されていることがわかる。

### 変数補間
通常の文字列と同様に変数補間を使うこともできる。ここで注意しなければならないのは、一つの文字列変数で保管された引数は、一つの引数として認識されてしまうということだ。例えばオプションとオプション引数を文字列でまとめたりすると、期待通りには動かない。

```Julia
julia> opt="-I /usr/include"
"-I /usr/include"
julia> `cc $opt test.c`
`cc '-I /usr/include' test.c`
```

複数の引数をまとめて参照したければ、文字列配列にすればいい。

```Julia
julia> opt=["-I", "/usr/include"]
2-element Array{String,1}:
 "-I"
 "/usr/include"

julia> `cc $opt test.c`
`cc -I /usr/include test.c`
```

### 実行結果の文字列への取り込み
外部コマンドの実行結果を文字列として取り込むには`read`関数を用いる。

```Julia
julia> ls_str = read(`echo test`, String)
"test\n"
```

なぜこの関数の名前を`read`にするのか、全く理解できない。非常に探しにくいので、ちょっと考えてほしかった。ちなみに起動されたプログラムの標準エラーは、`run`の場合と同様に呼び出しプロセスのttyに出ていく。

### 外部コマンドの標準出力、標準入力の取得

外部コマンドへの標準入力へ何かを書いたり、標準入力から読むためには`open`を使う。オプションで`read`,`write`を`true`にすると、標準入力、出力をプロセス構造体の`in`,`out`として参照できるようになる。

```Julia
julia> proc = open(`cat`, read=true, write=true)
Process(`cat`, ProcessRunning)
julia> write(proc.in, "test");
julia> close(proc.in)
julia> read(proc.out)
4-element Array{UInt8,1}:
 0x74
 0x65
 0x73
 0x74
```

ちなみに、どうも標準エラーをとりだすことはできない。いかにも`err=true`とすればできそうなものだが、ソースを見ても用意されていない。ちょっと関数を足すだけで簡単にできそうなのに。。

この辺の設計思想が理解に苦しむところ。普通直行性を重んじて用意するだろう。。不思議。

### 追記[2021/4/18]: 標準エラーの取得
twitterで標準エラーも取得できることを教えていただいた。ありがとうございます。[こちらのコード](https://gist.github.com/genkuroki/c83e9d3d984dcd46996ada89e96421c2)参照。

`open`のインターフェイスではなく`pipeline`のインターフェイスを使うと、標準エラーの取得ができる。

```Julia
julia> err=Pipe()
Pipe(RawFD(0xffffffff) init => RawFD(0xffffffff) init, 0 bytes waiting)

julia> run(pipeline(ignorestatus(`ls --test`), stderr=err))
Process(`ls --test`, ProcessExited(1))

julia> close(err.in)

julia> readlines(err.out)
2-element Array{String,1}:
 "ls: illegal option -- -"
 "usage: ls [-@ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1%] [file ...]"

```

### Base.open の改変
`open`を書き変えてしまうという方法も。[ここ](https://github.com/JuliaLang/julia/blob/f9720dc2ebd6cd9e3086365f281e62506444ef37/base/process.jl#L350-L359)にソースがあるので、`eread`というオプションを勝手に追加する。

``` julia
module My
function open(cmds::Cmd, stdio::Base.Redirectable=devnull; write::Bool=false, read::Bool=!write, eread::Bool=false)
    err = eread ? Base.PipeEndpoint() : stderr
    if read && write
        stdio === devnull || throw(ArgumentError("no stream can be specified for `stdio` in read-write mode"))
        in = Base.PipeEndpoint()
        out = Base.PipeEndpoint()
        processes = Base._spawn(cmds, Any[in, out, err])
        processes.in = in
        processes.out = out
    elseif read
        out = Base.PipeEndpoint()
        processes = Base._spawn(cmds, Any[stdio, out, err])
        processes.out = out
    elseif write
        in = Base.PipeEndpoint()
        processes = Base._spawn(cmds, Any[in, stdio, err])
        processes.in = in
    else
        stdio === devnull || throw(ArgumentError("no stream can be specified for `stdio` in no-access mode"))
        processes = Base._spawn(cmds, Any[devnull, devnull, err])
    end
    if eread processes.err = err end
    return processes
end
end
```
とやると、

```Julia
julia> proc = My.open(`ls --test`, read=true, eread=true)
Process(`ls --test`, ProcessExited(1))

julia> readlines(proc.err)
2-element Array{String,1}:
 "ls: illegal option -- -"
 "usage: ls [-@ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1%] [file ...]"
```

というように使えるようになる。他のオプションとの組み合わせが想定通り動くのか検証できていないけど。