Spec-Zone.ru › Julia 1.8

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

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 выбрасывает 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 в качестве аргумента команды. Для интерполяции можно использовать $ так же, как и в строковых литералах (см. Строки):

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–2022 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.8/manual/running-external-programs/

Spec-Zone.ru

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