Запуск внешних программ
Julia заимствует обрамление обратной косой чертой для команд из оболочки, Perl и Ruby. Однако в Julia запись
julia> `echo hello` `echo hello`
отличается по нескольким аспектам от поведения в различных оболочках, Perl или Ruby:
- Вместо немедленного запуска команды, обрамление обратной косой чертой создает объект
Cmdдля представления команды. Вы можете использовать этот объект для соединения команды с другими через каналы,runего иreadилиwriteк нему. - При запуске команды Julia не захватывает ее вывод, если вы не организуете это специально. Вместо этого вывод команды по умолчанию отправляется в
stdout, как если бы использовался вызовlibc'ssystem. - Команда никогда не запускается с оболочкой. Вместо этого 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. Если внешняя команда не выполняется успешно, метод 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"
Интерполяция
Предположим, что вы хотите сделать что-то немного более сложное и использовать имя файла в переменной 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`
Цитрование
Неизбежно возникает необходимость написать команды, которые не совсем просты, и становится необходимо использовать кавычки. Вот простой пример перловского однострочника в командной строке:
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 его все равно).
Сложный пример
Сочетание языка высокого уровня, абстракции команды первого класса и автоматической настройки каналов между процессами — мощный инструмент. Чтобы дать представление о сложных каналах, которые легко создать, здесь приведены несколько более сложных примеров с извинениями за чрезмерное использование перловских однострочников:
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 для дополнительных ключевых аргументов. Команды 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–2024 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.10/manual/running-external-programs/