Julia. Скрипти і розбір аргументів командного рядка
Продовжуємо розбиратися з мовою програмування Julia. Оскільки для мови, орієнтованого на аналіз і обробку даних, просто необхідно мати пакетний режим роботи, розглянемо особливості реалізації скриптів на мові Julia і передачі їм аргументів командного рядка. Комусь, може бути, ця тема здасться банальністю, але, враховуючи новизну мови, сподіваюся, що невеликий огляд способів розбору параметрів командного рядка і бібліотек для цього, представлених у Julia, все таки виявиться корисним.
Для початку, кілька слів про те, як оформляється скрипт. Будь-який скрипт починається з рядка спеціального формату, що вказує інтерпретатор. Рядок починається з послідовності, відомої як шебанг (Shebang). Для Julia такий рядком є:
#!/usr/bin/env julia
Звичайно, це можна і не робити, але тоді доведеться запускати скрипт командою:
julia имяскрипта.jl
Також, будь-скрипт повинен завершуватися символом переведення рядка. Це вимога стандарту 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». А все разом це називається Кульок.
Серед шаблонів можливе маскування декількох символів *, маскування одного символу ?.. Пошук по діапазону […], І, навіть, можливість вказати складні комбінації:
>./args.jl {args,doc}*
typeof(ARGS) = Array{String,1}
ARGS = ["args.jl", "docopt.jl"]
Детальніше див. документацію GNU/Linux Command-Line Tools Summary.
Якщо, з якоїсь причини, ми не хочемо використовувати механізм кульок, що надається bash, то знайти файли по масці можна вже з скрипта з допомогою пакета Кульок.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 begin s
"--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 <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)
Іменовані аргументи help
, version
, oprtions_first
, exit_on_error
задають поведінка парсера аргументів командрой рядка за замовчуванням. Наприклад, при помилках — завершувати виконання, на запит версії видавати підставлене тут значення version=...
, на запит -h
— видавати довідку. options_first
використовується для вказівки того, що опції повинні знаходитися до позиційних аргументів.
А тепер розглянемо докладніше цей декларативний мову і реакцію парсера аргументів на введені значення.
Декларація починається з довільного тексту, який, крім тексту для командного рядка, може бути частиною документації самого скрипта. Службове слово «Usage:» декларує шаблони варіантів використання даного скрипта.
Usage:
naval_fate.jl ship new <name>...
naval_fate.jl ship <name> move <x> <y> [--speed=<kn>]
Аргументи декларуються у формі <name>
, <x>
, <y>
. Зверніть увагу на те, що в асоціативному масиві args
, який був отриманий раніше, ці аргументи виступають у ролі ключів. Ми використовували форму запуску ./docopt.jl ship new Bystriy
, тому отримали наступні явно инициализированные значення:
"<name>"=>["Bystriy"],
"ship"=>true,
"new"=>true,
У відповідності з мовою docopt, додаткові елементи задаються у квадратних дужках. Наприклад [--speed=<kn>]
. У круглих дужках ставлять обов’язкові елементи, але з певною умовою. Наприклад (set|remove)
задає вимога наявності одного з них. Якщо ж елемент вказана без дужок, наприклад naval_fate.jl --version
, це говорить, що в конкретно цьому варіанті запуску --version
є обов’язковою опцією.
Наступна секція — це секція опису опцій. Вона починається зі слова «Options:»
Опції декларуються кожна на окремому рядку. Відступи зліва від початку рядка важливі. Для кожної опції можна вказати повну і коротку форму. А також видається в підказці опис опції. При цьому, опції -h | --help, --version
розпізнаються автоматично. Реакція на них задається аргументами функції docopt
. Цікавим для розгляду є декларація:
--speed=<kn> Speed in knots [default: 10].
Тут форма ...=<kn>
задає наявність деякого значення, а [default: 10]
визначає значення за замовчуванням. Звернемося знову до значень, отриманих в args
:
"--speed"=>"10"
Принциповою відмінністю, наприклад, від пакета ArgParse, є те, що значення не типизированы. Тобто значення default: 10
виставлено як рядок “10”.
Стосовно ж інших аргументів, які представлені в args
як результат аналізу аргументів, слід звернути увагу на їх значення:
"remove"=>false,
"--help"=>false,
"--drifting"=>false,
"mine"=>false,
"move"=>false,
"--version"=>false,
"--moored"=>false,
"<x>"=>nothing,
"shoot"=>false,
"set"=>false,
"<y>"=>nothing,
Тобто, абсолютно всі елементи шаблону, задані в декларації docopt для всіх варіантів використання, представлені в результаті розбору з вихідними іменами. Всі необов’язкові аргументи, які не були присутні в командному рядку, тут мають значення false. Аргументи <x>
, <y>
також відсутні в рядку запуску і мають значення nothing. Інші ж аргументи, для яких співпав шаблон розбору, отримали значення true:
"ship"=>true,
"new"=>true,
І вже конкретні значення ми отримали для наступних елементів шаблону:
"<name>"=>["Bystriy"],
"--speed"=>"10"
Перше значення було задано явно в командному рядку як підстановка аргументу , а друге — опція зі значенням за замовчуванням.
Також зверніть увагу на те, що ім’я поточного скрипта можна обчислити автоматично.
Наприклад, ми можемо вписати:
doc = """Naval Fate.
Usage:
$(Base.basename(@__FILE__)) ship new <name>...
"""
Додаткової рекоммендацией до розміщення парсера аргументів командного рядка є його розміщення в самому початку файлу. Неприємною особливістю Julia в даний момент є досить довгий підключення модулів. Наприклад using Plots; using DataFrames
може відправити скрипт в очікування на кілька секунд. Це не є проблемою для серверних, одноразово завантажуваних скриптів, але це буде дратувати користувачів, які просто хочуть подивитися підказку по аргументам командного рядка. Саме тому спочатку треба видавати довідку і перевіряти аргументи командного рядка, а лише потім приступати до завантаження необхідних для роботи бібліотек.
Висновок
Стаття не претендує на повноту розгляду всіх способів розбору аргументів у Julia. Однак розглянуті варіанти, по суті, покривають 3 можливих варіанта. Повністю ручний розбір масиву ARGS
. Строго задекларовані, але автоматично розібрані аргументи в ArgParse. І повністю декларативна, хоча і не сувора форма docopt. Вибір варіанту використання повністю залежить від складності розглядуваних аргументів. Варіант з використанням docopt бачиться найбільш простим у використанні, хоча і вимагає явного перетворення типів для отриманих значень аргументів. Однак, якщо скрипт не приймає нічого, крім імені файла, то, цілком, можна скористатися видачею довідки за нього за допомогою звичайної функції println("Run me with file name")
, а імена файлів розібрати безпосередньо з ARGS
так, як це було показано в першому розділі.
Посилання
- https://github.com/vtjnash/Glob.jl
- https://argparsejl.readthedocs.io/en/latest/argparse.html
- https://github.com/carlobaldassi/ArgParse.jl/
- http://docopt.org/
- https://github.com/docopt/DocOpt.jl