Running External Programs

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

julia> `echo hello`
`echo hello`

отличается в нескольких аспектах от поведения в различных оболочках, Perl или Ruby:

  • Вместо немедленного выполнения команды, обратные кавычки создают объект Cmd, чтобы представить команду. Вы можете использовать этот объект, чтобы соединить команду с другими через каналы, run, и read или write с ним.
  • Когда команда выполняется, Julia не захватывает ее вывод, если вы специально не организуете это. Вместо этого вывод команды по умолчанию отправляется в stdout, как это было бы при использовании вызова system из libc.
  • Команда никогда не выполняется с помощью оболочки. Вместо этого Julia разбирает синтаксис команды напрямую, соответствующим образом интерполируя переменные и разбивая на слова, как это делает оболочка, соблюдая синтаксис кавычек оболочки. Команда выполняется как непосредственный дочерний процесс julia, используя вызовы fork и exec.
Note

Следующее предполагает наличие среды 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. Если внешняя команда не удается выполнить успешно, метод 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 должно быть в одинарных кавычках по двум причинам: чтобы пробелы не разбивали выражение на несколько слов оболочки, и чтобы использование переменных 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 показывает команды вам перед их выполнением, вы можете легко и безопасно просто изучить ее интерпретацию, не нанося никакого ущерба.

Pipelines

Метасимволы оболочки, такие как |, & и >, необходимо экранировать (или заключать в кавычки) внутри обратных кавычек 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> 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.

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), поскольку первое активно потребляет все данные, записанные процессом, в то время как второе пытается сохранить данные в буферах ядра, ожидая подключения читателя.

Еще одно распространенное решение заключается в том, чтобы разделить читателя и писателя конвейера на отдельные Tasks:

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

Это классический пример одного производителя, который подает два параллельных потребителя: один процесс perl генерирует строки с числами от 0 до 5, в то время как два параллельных процесса потребляют этот вывод, один добавляя к строкам префикс "A", а другой - префикс "B". Какой потребитель получит первую строку, не поддается детерминизму, но как только эта гонка будет выиграна, строки потребляются поочередно одним процессом, а затем другим. (Установка $|=1 в Perl заставляет каждое выражение 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 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!