Spec-Zone.ru › Julia 1.11

Запуск внешних программ

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

julia> `echo hello`
`echo hello`

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

  • Вместо немедленного запуска команды, обратные кавычки создают объект Cmd для представления команды. Вы можете использовать этот объект для подключения команды к другим через конвейеры, run её, и read или write к ней.
  • При запуске команды Julia не захватывает её вывод, если вы не организуете это явно. Вместо этого вывод команды по умолчанию отправляется в stdout, как и при использовании libc's вызова 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

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`

Цитаты

Неизбежно, что нужно написать команды, которые не так просты, и становится необходимо использовать кавычки. Вот простой пример 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 для дополнительных ключевых аргументов. Команды 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.11/manual/running-external-programs/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API