Хабрахабр

Julia. Скрипты и разбор аргументов командной строки

Поскольку для языка, ориентированного на анализ и обработку данных, просто необходимо иметь пакетный режим работы, рассмотрим особенности реализации скриптов на языке Julia и передачи им аргументов из командной строки.
Продолжаем разбираться с языком программирования Julia. Кому-то, может быть, эта тема покажется банальностью, но, учитывая новизну языка, надеюсь, что небольшой обзор способов разбора аргументов командной строки и библиотек для этого, представленных в Julia, всё таки окажется полезным.

Любой скрипт начинается со строки специального формата, указывающей интерпретатор. Для начала, несколько слов о том, как оформляется скрипт. Для Julia такой строкой является: Строка начинается с последовательности, известной как шебанг (Shebang).

#!/usr/bin/env julia

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

julia имяскрипта.jl

Это требование стандарта POSIX, которое следует из определения строки как последовательности символов, завершенной символом перевода строки. Также, любой скрипт должен завершатьcя символом перевода строки.

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

chmod +x имяскрипта.jl

Эти правила справедливы для всех современных операционных систем, кроме, разве что, MS Windows.

Массив ARGS

Аргументы командной строки доступны в Julia-скрипте через константу-массив Base. Перейдём к первому варианту передачи параметров. Подготовим простейший скрипт: ARGS.

#!/usr/bin/env julia @show typeof(ARGS)
@show ARGS

Этот скрипт просто выводит в консоль тип и содержимое массива ARGS.

И здесь есть особенность обработки шаблона файла, передаваемого в качеств аргумента. Очень часто в качестве аргументов командной строки передают имя файла. Например, запустим наш скрипт при помощи команды ./args.jl *.jl и получим:

>./args.jl *.jl
typeof(ARGS) = Array
ARGS = ["argparse.jl", "args.jl", "docopt.jl"]

В результате получим: А теперь немного изменим параметр командной строки, окружив маску кавычками:
./args.jl "*.jl".

>./args.jl "*.jl"
typeof(ARGS) = Array{String,1}
ARGS = ["*.jl"]

В первом случае мы получили массив с именами всех файлов, которые находятся в той же директории. Видим очевидную разницу. Причина такого различного поведения скрипта заключается в том, что интерпретатор bash (а также близкие к нему), из которого и запускался скрипт, распознаёт шаблоны имён файлов. Во втором случае — это лишь та же маска, что была передана в качестве аргумента командной строки. А всё вместе это называется Globs. Подробнее можно найти в поисковике по запросу «Bash Pattern Matching» или «Bash Wildcards».

Поиск по диапазону [...], И, даже, возможность указать сложные комбинации: Среди шаблонов возможно маскирование нескольких символов — *, маскирование одного символа — ?..

>./args.jl {args,doc}*
typeof(ARGS) = Array{String,1}
ARGS = ["args.jl", "docopt.jl"]

документацию GNU/Linux Command-Line Tools Summary. Подробнее см.

То есть, независимо от того, задал ли пользователь маски в кавычках, без кавычек, или просто перечислил имена существующих или несуществующих файлов, в результирующем массиве filelist останутся только имена реально присутствующих файлов или директорий. Если, по какой-то причине, мы не хотим использовать механизм globs, предоставляемый bash, то найти файлы по маске можно уже из скрипта с помощью пакета Globs.jl.
Следующий код преобразует всё, что найдено в строке аргументов, в единый массив имён файлов.

using Glob filelist = unique(collect(Iterators.flatten(map(arg -> glob(arg), ARGS))))

Этот подход часто используется тогда, когда набор аргументов чрезвычайно простой. Эти простые примеры, по сути, и являются демонстрацией использования массива ARGS, где всю логику разбора аргументов реализует программист. Или одна-две опции, которые могут быть обработаны простыми строковыми операциями. Например перечень имён файлов. Помните только о том, что индекс первого элемента массива в Julia — 1. Доступ к элементам ARGS осуществляется так же, как и к элементам любого другого массива.

Пакет ArgParse.jl

Является гибким средством описания атрибутов и опций командной строки без необходимости реализации логики разбора.
Воспользуемся немного модифицированным примером из документации пакета — http://carlobaldassi.github.io/ArgParse.jl/stable/ :

#!/usr/bin/env julia using ArgParse function parse_commandline() s = ArgParseSettings() @add_arg_table s begin "--opt1" help = "an option with an argument" "--opt2", "-o" help = "another option with an argument" arg_type = Int default = 0 "--flag1" help = "an option without argument, i.e. a flag" action = :store_true "arg1" help = "a positional argument" required = true end return parse_args(s)
end function main() @show parsed_args = parse_commandline() println("Parsed args:") for (arg,val) in parsed_args print(" $arg => ") show(val) println() end
end main()

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

>./argparse.jl required argument arg1 was not provided
usage: argparse.jl [--opt1 OPT1] [-o OPT2] [--flag1] arg1

В то время, как аргумент, отмеченный как arg1 (то есть то, что мы подставим вместо него), является обязательным. Причём, в квадратных скобках мы видим опциональные аргументы.

Запустим ещё раз, но укажем обязательный атрибут arg1.

>./argparse.jl test
parsed_args = parse_commandline() = Dict{String,Any}("flag1"=>false,"arg1"=>"test","opt1"=>nothing,"opt2"=>0)
Parsed args: flag1 => false arg1 => "test" opt1 => nothing opt2 => 0

Причём значения имеют тот тип, который явно указан при декларации. Мы можем видеть, что parsed_args — это ассоциативный массив, где ключи — имена атрибутов согласно декларации, сделанной в функции parse_commandline, а их значения — то, что было подставлено по-умолчанию или передано в качестве значений аргументов командной строки.

Возможно декларировать опции : Декларация аргументов выполняется при помощи макроса @add_arg_table.

"--opt2", "-o" help = "another option with an argument" arg_type = Int default = 0

Или аргументы

"arg1" help = "a positional argument" required = true

Либо, только в единственной форме. Причем опции могут быть заданы с указанием полной и краткой формы (одновременно --opt2 и -o). Значение по-умолчанию может быть задано при помощи default = .... Тип указывается в поле arg_type. Это делается с помощью action = :store_true Альтернативой значению по-умолчанию является требование наличия аргумента — required = true.
Возможно задекларировать автоматическое действие, например присваивать true или false в зависимости от наличия или отсутствия аргумента.

"--flag1" help = "an option without argument, i.e. a flag" action = :store_true

Поле help содержит текст, который будет отображаться в подсказке в командной строке.
Если при запуске мы укажем все атрибуты, то получим:

>./argparse.jl --opt1 "2+2" --opt2 "4" somearg --flag
parsed_args = parse_commandline() = Dict{String,Any}("flag1"=>true,"arg1"=>"somearg","opt1"=>"2+2","opt2"=>4)
Parsed args: flag1 => true arg1 => "somearg" opt1 => "2+2" opt2 => 4

Для отладки из IDE Atom/Juno в первые строки скрипта можно добавить следующий, несколько грязный, но работающий код инициализации массива ARGS.

if (Base.source_path() != Base.basename(@__FILE__)) vcat(Base.ARGS, ["--opt1", "2+2", "--opt2", "4", "somearg", "--flag"] )
end

И это имя для REPL отличается от имени текущего файла программы, полученного через Base.source_path(). Макрос @__FILE__ — это имя файла, в котором макрос развернут. ARGS другим значением невозможно, но, при этом, можно добавить новые строки, поскольку сам массив не является константой. Инициализировать константу-массив Base. Массив — это столбец для Julia, поэтому используем vcat(vertical concatenate).

Но их придётся менять каждый раз для каждого отлаживаемого скрипта индивидуально. Впрочем, в настройках редактора Juno можно установить аргументы для запуска скрипта.

Пакет DocOpt.jl

Основная идея этого языка — декларативное описание опций и аргументов в форме, которая может являться и внутренним описанием скрипта. Этот вариант является реализацией подхода языка разметки docopt — http://docopt.org/. Используется специальный шаблонный язык.

Воспользуемся примером из документации к этому пакету https://github.com/docopt/DocOpt.jl

#!/usr/bin/env julia doc = """Naval Fate. Usage: naval_fate.jl ship new <name>... naval_fate.jl ship <name> move <x> <y> [--speed=<kn>] naval_fate.jl ship shoot <x> <y> naval_fate.jl mine (set|remove) <x> <y> [--moored|--drifting] naval_fate.jl -h | --help naval_fate.jl --version Options: -h --help Show this screen. --version Show version. --speed=<kn> Speed in knots [default: 10]. --moored Moored (anchored) mine. --drifting Drifting mine. """ using DocOpt # import docopt function args = docopt(doc, version=v"2.0.0")
@show args

Итогом запуска в командной строке без аргументов будет: Запись doc = ... — это создание Julia-строки doc, в которой содержится вся декларация для docopt.

>./docopt.jl Usage: naval_fate.jl ship new <name>... naval_fate.jl ship <name> move <x> <y> [--speed=<kn>] naval_fate.jl ship shoot <x> <y> naval_fate.jl mine (set|remove) <x> <y> [--moored|--drifting] naval_fate.jl -h | --help naval_fate.jl --version

Если же воспользуемся подсказкой и попытаемся «создать новый корабль», то получим распечатку ассоциативного массива args, который был сформирован результом разбора командной строки

>./docopt.jl ship new Bystriy
args = Dict{String,Any}( "remove"=>false, "--help"=>false, "<name>"=>["Bystriy"], "--drifting"=>false, "mine"=>false, "move"=>false, "--version"=>false, "--moored"=>false, "<x>"=>nothing, "ship"=>true, "new"=>true, "shoot"=>false, "set"=>false, "<y>"=>nothing, "--speed"=>"10")

Функция docopt декларируется как:

docopt(doc::AbstractString, argv=ARGS; help=true, version=nothing, options_first=false, exit_on_error=true)

Например, при ошибках — завершать выполнение, на запрос версии выдавать подставленное здесь значение version=…, на запрос -h — выдавать справку. Именованные аргументы help, version, oprtions_first, exit_on_error задают поведение парсера аргументов командрой строки по-умолчанию. options_first используется для указания того, что опции должны находиться до позиционных аргументов.

А теперь рассмотрим подробнее этот декларативный язык и реакцию парсера аргументов на введенные значения.

Служебное слово «Usage:» декларирует шаблоны вариантов использования данного скрипта. Декларация начинается с произвольного текста, который, помимо текста для командной строки, может являться частью документации самого скрипта.

Usage: naval_fate.jl ship new <name>... naval_fate.jl ship <name> move <x> <y> [--speed=<kn>]

Обратите внимание на то, что в ассоциативном массиве args, который был получен ранее, эти аргументы выступают в роли ключей. Аргументы декларируются в форме <name>, <x>, <y>. Мы использовали форму запуска ./docopt.jl ship new Bystriy, поэтому получили следующие явно инициализированные значения:

"<name>"=>["Bystriy"], "ship"=>true, "new"=>true,

Например [--speed=<kn>]. В соответствии с языком docopt, опциональные элементы задаются в квадратных скобках. Например (set|remove) задаёт требование наличия одного из них. В круглых скобках задаются обязательные элементы, но с определенным условием. Если же элемент указан без скобок, например naval_fate.jl --version, это говорит, что в конкретно этом варианте запуска --version является обязательной опцией.

Она начинается со слова «Options:»
Опции декларируются каждая на отдельной строчке. Следующая секция — это секция описания опций. Для каждой опции можно указать полную и краткую форму. Отступы слева от начала строки важны. При этом, опции -h | --help, --version распознаются автоматически. А также выдаваемое в подсказке описание опции. Интересной же для рассмотрения является декларация: Реакция на них задаётся аргументами функции docopt.

--speed=<kn> Speed in knots [default: 10].

Обратимся опять к значениям, полученным в args: Здесь форма ...=<kn> задаёт наличие некоторого значения, а [default: 10] определяет значение по умолчанию.

"--speed"=>"10"

То есть значение default: 10 выставлено как строка "10".
В отношении же прочих аргументов, которые представлены в args как результат разбора аргументов, следует обратить внимание на их значения: Принципиальным отличием, например, от пакета ArgParse, является то, что значения не типизированы.

"remove"=>false, "--help"=>false, "--drifting"=>false, "mine"=>false, "move"=>false, "--version"=>false, "--moored"=>false, "<x>"=>nothing, "shoot"=>false, "set"=>false, "<y>"=>nothing,

Все опциональные аргументы, которые не присутствовали в командной строке, здесь имеют значение false. То есть, абсолютно все элементы шаблона, заданные в декларации docopt для всех вариантов использования, представлены в результате разбора с исходными именами. Прочие же аргументы, для которых совпал шаблон разбора, получили значения true: Аргументы <x>, <y> также отсутствуют в строке запуска и имеют значение nothing.

"ship"=>true, "new"=>true,

И уже конкретные значения мы получили для следующих элементов шаблона:

"<name>"=>["Bystriy"], "--speed"=>"10"

Первое значение было задано явно в командной строке как подстановка аргумента , а второе — опция со значением по-умолчанию.

Также обратите внимание на то, что имя текущего скрипта можно вычислить автоматически.
Например, мы можем вписать:

doc = """Naval Fate. Usage: $(Base.basename(@__FILE__)) ship new <name>… """

Неприятной особенностью Julia в данный момент является довольно долгое подключение модулей. Дополнительной рекоммендацией к размещению парсера аргументов командной строки является его размещение в самом начале файла. Это не является проблемой для серверных, однократно загружаемых скриптов, но это будет раздражать пользователей, которые просто хотят посмотреть подсказку по аргументам командной строки. Например using Plots; using DataFrames может отправить скрипт в ожидание на несколько секунд. Именно поэтому, сначала надо выдавать справку и проверять аргументы командной строки, а, лишь потом, приступать к загрузке необходимых для работы библиотек.

Заключение

Однако рассмотренные варианты, по сути, покрывают 3 возможных варианта. Статья не претендует на полноту рассмотрения всех способов разбора аргументов в Julia. Строго задекларированные, но автоматически разбираемые аргументы в ArgParse. Полностью ручной разбор массива ARGS. Выбор варианта использования полностью зависит от сложности разбираемых аргументов. И полностью декларативная, хотя и не строгая форма docopt. Однако, если скрипт не принимает ничего, кроме имени файла, то, вполне, можно воспользоваться выдачей справки по нему при помощи обычной функции println("Run me with file name"), а имена файлов разобрать непосредственно из ARGS так, как это было продемонстрировано в первом разделе. Вариант с использованием docopt видится наиболее простым в использовании, хотя и требует явного преобразования типов для значений полученных аргументов.

Ссылки

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»