Документация Engee

Выполнение внешних программ

Julia заимствует нотацию обратного апострофа для команд из оболочки, Perl и Ruby. Однако в Julia написание

julia> `echo hello`
`echo hello`

имеет ряд отличий от поведения в различных оболочках, Perl или Ruby.

  • Вместо немедленного выполнения команды обратные апострофы создают объект Cmd для представления команды. Вы можете использовать этот объект для соединения команды с другими с помощью каналов передачи, ее выполнения с помощью функции run, ее чтения с помощью функции read или записи в нее с помощью функции write.

  • При выполнении команды Julia не записывает ее вывод, если вы не предусмотрели это специально. Вывод команды по умолчанию отправляется в константу stdout, как и при использовании вызова system библиотеки (libc).

  • Команда никогда не выполняется в оболочке. Julia анализирует синтаксис команды напрямую, соответствующим образом интерполируя переменные и выполняя разделение на слова, как это сделала бы оболочка, соблюдая синтаксис заключения в кавычки в оболочке. Команда выполняется как непосредственный дочерний процесс julia с помощью вызовов fork и exec.

В примерах далее предполагается наличие среды Posix, как в Linux или MacOS. В Windows многие подобные команды, такие как echo и dir, не являются внешними программами, а встроены в сам файл 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

hello является выводом команды echo, отправляемым в константу stdout. Если выполнение внешней команды завершается сбоем, метод выполнения вызывает исключение 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"

Интерполяция

Предположим, требуется сделать что-то более сложное и использовать имя файла в переменной 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 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 в приглашении оболочки.

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

Выражение Perl должно быть заключено в одинарные кавычки по двум причинам: чтобы пробелы не разбивали выражение на несколько слов оболочки и чтобы использование переменных Perl, таких как $| (да, это имя переменной в Perl), не приводило к интерполяции. В других случаях можно использовать двойные кавычки, чтобы интерполяция произошла.

sh$ first="A"
sh$ second="B"
sh$ 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. Команды cut, sort и tail порождаются как непосредственные дочерние элементы текущего процесса julia без промежуточного процесса оболочки. Julia самостоятельно выполняет работу по настройке передачи и подключению дескрипторов файлов, которую обычно делает оболочка. Поскольку Julia делает это своими силами, она обеспечивает максимальный контроль и может реализовывать то, что не под силу оболочкам.

Julia может выполнять несколько команд параллельно.

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

Порядок вывода здесь недетерминирован, поскольку два процесса echo запускаются почти одновременно и участвуют в гонке за право первой записи в дескриптор stdout, который является общим для них и родительского процесса julia. Julia позволяет передавать вывод из обоих этих процессов в другую программу.

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

С точки зрения конвейеризации UNIX здесь происходит то, что один объект канала передачи UNIX создается и записывается обоими процессами echo, а другой конец канала передачи считывается командой sort.

Перенаправление ввода-вывода может быть выполнено путем передачи именованных аргументов stdin, stdout и stderr функции 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. Какой потребитель получит первую строку, является недетерминированным, но после того, как эта гонка будет выиграна, строки поочередно используются то одним процессом, то другим. (При установке $|=1 в Perl каждый оператор вывода очищает дескриптор 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!

Список дополнительных именованных аргументов см. в описании 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!