Spec-Zone.ru › Julia 1.2

Выполнение внешних программ

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

julia> `echo hello`
`echo hello`

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

  • Вместо немедленного выполнения команды, обратные кавычки создают объект Cmd для представления команды. Вы можете использовать этот объект для соединения команды с другими через конвейеры, run его, и read или write к нему.
  • Когда команда выполняется, Julia не захватывает ее вывод, если вы не организуете это специально. Вместо этого вывод команды по умолчанию отправляется в stdout, как это происходило бы с использованием libc's system вызова.
  • Команда никогда не выполняется с оболочкой. Вместо этого Julia анализирует синтаксис команды напрямую, соответствующим образом интерполируя переменные и разбивая по словам так, как это делала бы оболочка, соблюдая синтаксис цитирования оболочки. Команда выполняется как непосредственный дочерний процесс julia с использованием fork и exec вызовов.

Вот простой пример выполнения внешней программы:

julia> mycommand = `echo hello`
`echo hello`

julia> typeof(mycommand)
Cmd

julia> run(mycommand);
hello

hello — это вывод команды echo, отправленный в stdout. Сам метод run возвращает nothing, и выбрасывает ErrorException, если внешняя команда не выполняется успешно.

Если вы хотите прочитать вывод внешней команды, можно использовать read:

julia> a = read(`echo hello`, String)
"hello\n"

julia> chomp(a) == "hello"
true

Более обще, вы можете использовать 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 Array{String,1}:
 "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 Array{String,1}:
 "/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 Array{String,1}:
 "foo"
 "bar"
 "baz"

julia> `grep xylophone $names.txt`
`grep xylophone foo.txt bar.txt baz.txt`

Кроме того, если вы интерполируете несколько массивов в одно и то же слово, эмулируется поведение генерации декартовых произведений оболочки:

julia> names = ["foo","bar","baz"]
3-element Array{String,1}:
 "foo"
 "bar"
 "baz"

julia> exts = ["aux","log"]
2-element Array{String,1}:
 "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(process)
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–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.2.0/manual/running-external-programs/

Spec-Zone.ru

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