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

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

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

#!/usr/bin/env julia

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

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

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

Для того, чтобы скрипт можно было непосредственно запустить, необходимо наличие у него атрибута 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{String,1}
ARGS = ["argparse.jl", "args.jl", "docopt.jl"]

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

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

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

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

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

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

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

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

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


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

        "--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

Макрос @__FILE__ — это имя файла, в котором макрос развернут. И это имя для REPL отличается от имени текущего файла программы, полученного через Base.source_path(). Инициализировать константу-массив Base.ARGS другим значением невозможно, но, при этом, можно добавить новые строки, поскольку сам массив не является константой. Массив — это столбец для 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 ...
  naval_fate.jl ship  move   [--speed=]
  naval_fate.jl ship shoot  
  naval_fate.jl mine (set|remove)   [--moored|--drifting]
  naval_fate.jl -h | --help
  naval_fate.jl --version

Options:
  -h --help     Show this screen.
  --version     Show version.
  --speed=  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 ...
  naval_fate.jl ship  move   [--speed=]
  naval_fate.jl ship shoot  
  naval_fate.jl mine (set|remove)   [--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,
  ""=>["Bystriy"],
  "--drifting"=>false,
  "mine"=>false,
  "move"=>false,
  "--version"=>false,
  "--moored"=>false,
  ""=>nothing,
  "ship"=>true,
  "new"=>true,
  "shoot"=>false,
  "set"=>false,
  ""=>nothing,
  "--speed"=>"10")

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

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

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

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

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

Usage:
  naval_fate.jl ship new ...
  naval_fate.jl ship  move   [--speed=]

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

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

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

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

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

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

"--speed"=>"10"

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

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

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

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

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

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

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

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

doc = """Naval Fate.

Usage:
  $(Base.basename(@__FILE__)) ship new …
"""

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


Заключение

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


Ссылки


© Habrahabr.ru