运行外部程序

Julia 从 shell、Perl 和 Ruby 中借鉴了反引号表示命令的用法。但是,在 Julia 中,编写

julia> `echo hello`
`echo hello`

在几个方面与各种 shell、Perl 或 Ruby 中的行为不同。

  • 反引号不会立即运行命令,而是创建一个 Cmd 对象来表示该命令。您可以使用此对象通过管道将命令连接到其他命令,run 它,以及 readwrite 到它。
  • 运行命令时,Julia 不会捕获其输出,除非您明确安排这样做。默认情况下,命令的输出将发送到 stdout,就像使用 libcsystem 调用一样。
  • 命令永远不会使用 shell 运行。相反,Julia 直接解析命令语法,适当地插值变量并在单词处拆分,就像 shell 一样,尊重 shell 引用语法。命令作为 julia 的直接子进程运行,使用 forkexec 调用。
注意

以下假设一个 Posix 环境,如 Linux 或 MacOS。在 Windows 上,许多类似的命令(例如 echodir)不是外部程序,而是 shell cmd.exe 本身内置的。运行这些命令的一个选项是调用 cmd.exe,例如 cmd /C echo hello。或者,可以在 Posix 环境(如 Cygwin)中运行 Julia。

以下是一个运行外部程序的简单示例

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

julia> typeof(mycommand)
Cmd

julia> run(mycommand);
hello

helloecho 命令的输出,发送到 stdout。如果外部命令无法成功运行,run 方法将抛出 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`

通过 shell 运行外部程序时的一个常见陷阱是,如果文件名包含对 shell 有特殊意义的字符,它们可能会导致意外行为。例如,假设我们想要对文件 /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 的值从未被 shell 解释过,因此不需要实际引用;引号仅用于向用户呈现。即使您将值作为 shell 词的一部分进行插值,这也将有效

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

如果您将数组作为 shell 词的一部分进行插值,Julia 将模拟 shell 的 {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`

此外,如果您将多个数组插值到同一个词中,将模拟 shell 的笛卡尔积生成行为

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`

引用

不可避免地,人们想要编写不太简单的命令,这时就需要使用引号。以下是在 shell 提示符下使用 Perl 一行代码的简单示例

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 管道而言,这里发生的事情是,创建一个单个 UNIX 管道对象,两个 echo 进程都写入该对象,并且 sort 命令从该管道的另一端读取数据。

IO 重定向可以通过将关键字参数 stdinstdoutstderr 传递给 pipeline 函数来实现

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

避免管道中的死锁

当从单个进程读取和写入管道的两端时,重要的是要避免强制内核缓冲所有数据。

例如,当读取命令的所有输出时,请调用read(out, String),而不是wait(process),因为前者会主动消耗进程写入的所有数据,而后者会尝试将数据存储在内核缓冲区中,同时等待读取器连接。

另一个常见的解决方案是将管道的读取器和写入器分离到单独的Task中。

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会导致每个 print 语句刷新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!