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