Запуск внешних программ
Julia заимствует нотацию с обратными кавычками для команд из оболочки, Perl и Ruby. Однако в Julia запись
julia> `echo hello` `echo hello`
отличается в нескольких аспектах от поведения в различных оболочках, Perl или Ruby:
- Вместо немедленного запуска команды, обратные кавычки создают объект
Cmdдля представления команды. Вы можете использовать этот объект для подключения команды к другим через конвейеры,runего, иreadилиwriteк нему. - При запуске команды Julia не захватывает ее вывод, если вы не организуете это специально. Вместо этого вывод команды по умолчанию направляется в
stdout, как если бы вы использовалиlibcвызовsystem. - Команда никогда не запускается с оболочкой. Вместо этого 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
Результат выполнения команды echo направляется в stdout. Если внешняя команда не выполняется успешно, метод run генерирует исключение ErrorException.
Если вы хотите прочитать вывод внешней команды, можно использовать 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 в качестве аргумента команды. Вы можете использовать $ для интерполяции так же, как и в строковой литерале (см. 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'`
Как имя файла оказалось в кавычках? 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 вызывает сброс каждого оператора 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!
См. [Cmd](@ref) для дополнительных ключевых аргументов. Команды 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!
© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.7.0/manual/running-external-programs/