執行外部程式

Julia 借用 shell、Perl 和 Ruby 中的後引號符號表示命令。然而,在 Julia 中,撰寫

julia> `echo hello`
`echo hello`

在幾個方面與各種 shell、Perl 或 Ruby 中的行為不同

  • 後引號不會立即執行命令,而是建立一個 Cmd 物件來表示命令。您可以使用此物件透過管線將命令連接到其他命令,run 它,以及 readwrite 到它。
  • 當指令執行時,Julia 只有在您特別安排的情況下才會擷取其輸出。反之,指令的輸出預設會傳送到 stdout,就像使用 libcsystem 呼叫一樣。
  • 指令絕不會使用殼層執行。反之,Julia 會直接剖析指令語法,適當地內插變數並在字詞上進行分割,就像殼層會做的那樣,並遵守殼層引號語法。指令會作為 julia 的立即子程序執行,使用 forkexec 呼叫。
注意

以下假設為 Linux 或 MacOS 上的 Posix 環境。在 Windows 上,許多類似的指令,例如 echodir,並非外部程式,而是內建於殼層 cmd.exe 本身。執行這些指令的一個選項是呼叫 cmd.exe,例如 cmd /C echo hello。或者,Julia 可以執行在 Posix 環境中,例如 Cygwin。

以下是一個執行外部程式的簡單範例

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

julia> typeof(mycommand)
Cmd

julia> run(mycommand);
hello

helloecho 指令的輸出,傳送到 stdout。如果外部指令無法成功執行,執行方法會擲回 ProcessFailedException

如果您想要讀取外部指令的輸出,可以使用 readreadchomp 來代替

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"

內插

假設您想做一些更複雜的事情,並使用變數 file 中的檔案名稱作為指令的引數。您可以使用 $ 進行內插,就像在字串文字中一樣(請參閱 字串

julia> file = "/etc/passwd"
"/etc/passwd"

julia> `sort $file`
`sort /etc/passwd`

透過殼層執行外部程式時,一個常見的陷阱是,如果檔案名稱包含對殼層來說特殊字元,它們可能會導致不良行為。例如,假設我們想要排序檔案 /Volumes/External HD/data.csv 的內容,而不是 /etc/passwd。我們來試試看

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 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`

加上引號

難免地,你會想要撰寫不那麼簡單的指令,而且必須使用引號。以下是 Perl 一行指令在 shell 提示字元中的簡單範例

sh$ perl -le '$|=1; for (0..3) { print }'
0
1
2
3

Perl 表達式需要在單引號中,原因有二:讓空白不將表達式分解成多個 shell 字詞,以及讓 Perl 變數(例如 $|,是的,這是 Perl 中變數的名稱)的使用不會造成內插。在其他情況下,你可能想要使用雙引號,讓內插確實發生

sh$ first="A"
sh$ second="B"
sh$ perl -le '$|=1; print for @ARGV' "1: $first" "2: $second"
1: A
2: B

一般而言,Julia 反引號語法經過仔細設計,讓你只要將 shell 指令剪下並貼上到反引號中即可,而且它們會運作:跳脫、引號和內插行為與 shell 的相同。唯一的差異是內插是整合的,而且知道 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 的內插行為模擬 shell 的行為,並因為 Julia 支援第一類可迭代物件而有一些改進,而大多數 shell 使用在此分割字串的空白,這會造成歧義。當嘗試將 shell 指令移植到 Julia 時,請先嘗試剪下和貼上。由於 Julia 在執行指令前會顯示指令給你,你可以輕鬆且安全地檢查它的詮釋,而不會造成任何損害。

管線

Shell 超字元,例如 |&>,需要在 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。cutsorttail 指令都作為目前 julia 程序的立即子程序產生,沒有介入的 shell 程序。Julia 本身會執行設定管線和連接檔案描述子的工作,這通常是由 shell 執行的。由於 Julia 本身執行這項工作,因此它能保留更好的控制權,並能執行 shell 無法執行的某些動作。

Julia 可以平行執行多個指令

julia> run(`echo hello` & `echo world`);
world
hello

這裡輸出的順序是非決定性的,因為兩個 echo 程序幾乎同時啟動,並競相對它們與 julia 父程序共用的 stdout 描述子進行第一次寫入。Julia 讓您可以將這兩個程序的輸出傳輸到另一個程序

julia> run(pipeline(`echo world` & `echo hello`, `sort`));
hello
world

就 UNIX 管線而言,這裡發生的情況是,由兩個 echo 程序建立並寫入一個 UNIX 管線物件,而管線的另一端則由 sort 指令讀取。

IO 重定向可透過傳遞關鍵字參數 stdinstdoutstderrpipeline 函式來達成

pipeline(`do_work`, stdout=pipeline(`sort`, "out.txt"), stderr="errs.txt")

避免管道中的死結

從單一程序讀取和寫入管道的兩端時,避免強制核心緩衝所有資料非常重要。

例如,讀取指令的所有輸出時,請呼叫 read(out, String),而非 wait(process),因為前者會主動使用程序寫入的所有資料,而後者會嘗試將資料儲存在核心的緩衝區中,同時等待讀取器連線。

另一種常見的解決方案是將管道的讀取器和寫入器分開成不同的 Tasks

writer = @async write(process, "data")
reader = @async do_compute(read(process, String))
wait(writer)
fetch(reader)

(通常讀取器也不是一個獨立的任務,因為我們會立即 fetch 它)。

複雜範例

結合高階程式語言、一級指令抽象以及程序之間管道的自動設定,將會非常強大。為了說明可以輕鬆建立的複雜管道,以下提供一些更精密的範例,對於過度使用 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 會導致每個列印陳述式清除 stdout 處理,這對於此範例的運作是必要的。否則,所有輸出都會緩衝並一次列印到管道,只會由一個使用者程序讀取。)

以下是更複雜的多階段生產者-消費者範例

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

此範例類似於前一個範例,但有兩個階段的消費者,且階段有不同的延遲,因此使用不同數量的平行工作者來維持飽和的通量。

我們強烈建議您嘗試所有這些範例,看看它們如何運作。

Cmd 物件

反引號語法建立 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!

有關其他關鍵字引數,請參閱 Cmdsetenvaddenv 命令分別提供取代或新增到 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!