Запуск внешних программ
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
Результат выполнения команды echo направлен в stdout. Метод run сам возвращает nothing, и выбрасывает 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)
Сложный пример
Сочетание языка высокого уровня, абстракции команды первого класса и автоматической установки каналов между процессами — мощный инструмент. Чтобы дать представление о сложных каналах, которые можно легко создать, здесь приведены несколько более сложных примеров, с извинениями за чрезмерное использование однострочных команд 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
Этот пример аналогичен предыдущему, за исключением того, что есть два этапа потребителей, и этапы имеют разную задержку, поэтому они используют разное количество параллельных рабочих процессов для поддержания насыщенной пропускной способности.
Мы настоятельно рекомендуем вам попробовать все эти примеры, чтобы увидеть, как они работают.
© 2009–2021 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.6.0/manual/running-external-programs/