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

 

Читайте також  Віконце з кнопками на JavaFX:

Ці прості приклади, по суті, і є демонстрацією використання масиву 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

 

Читайте також  Як використовувати нову експериментальну функцію Profiler в React

Макрос @__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 є обов’язковою опцією.

Читайте також  Lisp зі смаком Pascal або 8501-й мова програмування

 

Наступна секція — це секція опису опцій. Вона починається зі слова «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

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *