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 видится наиболее простым в использовании, хотя и требует явного преобразования типов для значений полученных аргументов.