Створюємо консольні утиліти на Ruby

8 хв. читання

Більшість утиліт, якими користуються розробники, виконані в вигляді консольного додатка. Ці утиліти можуть виконувати як тривіальні, так і комплексні завдання. До того ж, роботу декількох консольних утиліт можна об'єднувати в ланцюжок (пайп, pipe). Тому володіння командним рядком та його утилітами позитивно вплинуть на вашу продуктивність.

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

Зазвичай для таких специфічних функцій використовують готові бібліотеки, де вже реалізована робота на різних архітектурах.

Компоненти консольного додатка на Ruby

Є три частини консольного додатка, на які потрібно звернути увагу: вхідні дані, їх вивід та весь процес посередині.

Обробка вхідних даних

Перш за все, потрібно обробляти параметри, що можуть бути передані нашому додатку. Параметри — це те, що визначає як повинна працювати наша утиліта і те, що допомагає уникнути додаткових меню.

ruby -e "puts RUBY_VERSION"

В даному прикладі ruby — програма, яку ми запускаємо, -e — назва параметру, а "puts RUBY_VERSION" — його значення. В даному випадку -e означає, що переданий рядок слід виконати як Ruby-код. Якщо запустити цю команду, то вона виведе версію вашого інтерпретатора (в мене це 2.4.0).

Аргументи

Аргументи (або параметри) — весь той текст, що йде після назви утиліти. Вони розділяються між собою пробілом. Більшість програм також підтримують параметр для виводу допомоги. Параметри починаються с одного або двох дефісів.

Зазвичай для виводу довідки використовують параметр -h та --help. Повертаючись в часи MSDOS, ви можете згадати, що параметри вказувались за допомогою слешу (/?). Не знаю, чи до сьогодні в Windows так вказуються параметри, але сьогодні всі кросплатформні скрипти використовують дефіси.

В Ruby параметри, передані при запуску, зберігаються в двох змінних: ARGV та ARGF. В першій знаходяться аргументи в вигляді масиву, в другій — у вигляді потоку даних. Ви можете обробляти ARGV вручну, але зазвичай так не роблять. Існує купа готових бібліотек, що роблять всю брудну роботу за вас.

OptionParser

OptionParser має перевагу перед рештою, адже він вже включений в Ruby, вам не потрібно встановлювати нічого нового. OptionParser дозволяє і зручно оголошувати, які параметри утиліта приймає, і обробляти передані параметри. Ось приклад з однієї з моїх утиліт:

require 'optionparser'
options = {}
printers = Array.new

OptionParser.new do |opts|
  opts.banner = "Usage: dfm [options] [path]\
Defaults: dfm -xd ." + File::SEPARATOR
  opts.on("-f", "--filters FILTERS", Array, "File extension filters") do |filters|
    options[:filters] = filters
  end
  opts.on("-x", "--duplicates-hex", "Prints duplicate files by MD5 hexdigest") do |dh|
    printers << "dh"
  end
  opts.on("-d", "--duplicates-name", "Prints duplicate files by file name") do |dh|
    printers << "dn"
  end
  opts.on("-s", "--singles-hex", "Prints non-duplicate files by MD5 hexdigest") do |dh|
    printers << "sh"
  end
  opts.on("-n", "--singles-name", "Prints non-duplicate files by file name") do |dh|
    printers << "sn"
  end
end.parse!

Тут кожен блок opts.on має код, який виконається якщо аргумент буде передано. Останні чотири просто додають елемент до масиву, щоб використовувати його пізніше.

Перший блок приймає масив, тому цей аргумент Ruby буде розглядати як масив, а отримані дані помістить в об'єкт options під ключем filters.

Решта параметрів, що передаються в on — опис та деталі про параметри. OptionParser вже з коробки підтримує вивід довідки.

$ dfm -h
Usage: dfm [options] [path]
Defaults: dfm -xd ./
    -f, --filters FILTERS            File extension filters
    -x, --duplicates-hex             Prints duplicate files by MD5 hexdigest
    -d, --duplicates-name            Prints duplicate files by file name
    -s, --singles-hex                Prints non-duplicate files by MD5 hexdigest
    -n, --singles-name               Prints non-duplicate files by file name

Slop

OptionParser досить багатослівний, особливо коли вам потрібно написати невеличкий додаток. Для таких випадків був створений Slop, щоб вам було зручніше писати свої утиліти. Замість написання блоку коду, який повинен виконуватися при передачі кожного аргументу, ви просто отримуєте об'єкт з вже розібраними параметрами, який можете використовувати в своєму коді:

opts = Slop.parse do |o|
  o.string '-h', '--host', 'a hostname'
  o.integer '--port', 'custom port', default: 80
  o.bool '-v', '--verbose', 'enable verbose mode'
  o.bool '-q', '--quiet', 'suppress output (quiet mode)'
  o.bool '-c', '--check-ssl-certificate', 'check SSL certificate for host'
  o.on '--version', 'print the version' do
    puts Slop::VERSION
    exit
  end
end

ARGV #=> -v --host 192.168.0.1 --check-ssl-certificate

opts[:host]                 #=> 192.168.0.1
opts.verbose?               #=> true
opts.quiet?                 #=> false
opts.check_ssl_certificate? #=> true

opts.to_hash  #=> { host: "192.168.0.1", port: 80, verbose: true, quiet: false, check_ssl_certificate: true }

Вивід (stdout)

Навіть найпростіший додаток часто потребує виводити дані в консоль.

Ви можете форматувати ці дані як текстовий рядок, список, хеш, вкладений масив, JSON, XML тощо. І якщо ці дані повинен побачити користувач, то нам потрібно привести їх до зручного вигляду.

Прикладом такого виводу може стати утиліта для тестування API. Багато API віддають відповідь в JSON, який можна отримати за допомогою curl або подібних утиліт. А потім його можна форматувати для більш зручного відображення:

x = {"hello" => "world", this: {"apple" => 4, tastes: "delicious"}}

require 'json'
puts x.to_json
# {"hello":"world","this":{"apple":4,"tastes":"delicious"}}

puts JSON.pretty_generate( x )
# {
#   "hello": "world",
#   "this": {
#     "apple": 4,
#     "tastes": "delicious"
#   }
# }

А можливо, ви захочете використовувати YAML для даних:

require 'yaml'
puts x.to_yaml
# ---
# hello: world
# :this:
#   apple: 4
#   :tastes: delicious

Якщо ви хочете більш гнучкий вивід, то вам слід скористатися awesome_print.

Вивід помилок (stderr)

Це теж один із видів виводу даних. Якщо в програмі щось пішло не так, вона повинна сповістити про це повідомленням в спеціальний потік STDERR. Для користувача це виглядатиме як звичайний вивід, а от інші утиліти зможуть перевірити чи команда завершилася успішно.

STDERR.puts "Oops! You broke it!"

А ще краще використовувати Logger. Він допоможе детальніше оформити повідомлення про помилку і відобразити його в консолі чи записати в файл.

Користувацький інтерфейс (stdin та stdout)

Ця частина коду дає вам простір для фантазії. Саме тут можна виділити вашу утиліту якимось цікавим рішенням. Найпростішим способом взаємодії з користувачем – відображення запрошення до вводу та очікування якогось тексту. Це може виглядати так:

Хто ваш улюблений супергерой [Superman]/Batman/Wonder-Woman ?

В прикладі вище ми поставили питання, вказали варіанти відповіді і помітили варіант за умовчуванням, який буде застосовано якщо користувач не введе нічого. На Ruby це виглядає так:

favorite = "Superman"
printf "Хто ваш улюблений супергерой [Superman]/Batman/Wonder Woman?"
input = gets.chomp
favorite = input unless input.empty?

Це досить сирий код, лише для того щоб проілюструвати процес. Якщо ви будете писати повноцінний UI, я рекомендую використовувати highline або tty.

З highline приклад вище виглядає так:

require 'highline/import'
favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question|
  question.in = ["Superman", "Batman", "Wonder Woman"]
  question.default = "Superman"
}

Тут highline відобразить питання, варіанти відповіді, варіант за умовчуванням та відловить неправильні варіанти вводу від користувача.

І highline, і tty мають купу додаткових фіч, що можуть бути корисні при створенні меню та поліпшенні UX.

Чим більше UI в своєму додатку ви робите, тим більше уваги вам потрібно приділяти кросплатформності інструментів, які ви використовуєте. Не всі інструменти підтримують всі популярні ОС, а деякі працюють по різному на різних ОС, звертайте на це увагу.

Сумісність

Гарна новина! Ruby вже має багато вбудованих можливостей для адаптації вашого додатку під різні ОС. Коли Ruby компілюється для якоїсь системи, створюється файл RbConfig зі значеннями, специфічними для цієї ОС, збереженими в хеш-форматі. Це головний спосіб визначення системи та фіч, які вона підтримує.

Щоб відкрити його в вашому улюбленому текстовому редакторі, виконайте наступний Ruby-код:

editor = "sublime" # ваш улюблений редактор
exec "#{editor} #{RbConfig.method(:ruby).source_location[0]}"

Це відобразить файл в більш зручному вигляді, ніж просто вивід RbConfig::CONFIG. Цей об'єкт зберігає досить цікаві дані, наприклад, версія інтерпретатора, архітектура системи, де знаходяться ключові компоненти Ruby тощо.

Наприклад, так можна організувати перевірку ОС:

case RbConfig::CONFIG['target_os']
when /mingw32|mswin/
  # Ймовірно, це Windows 
else
  # Скоріше всього, Unix-подібна система, така як BSD, Mac чи Linux
end

Для більш точної перевірки можете використати цей код Gem::Platform.

В цьому файлі є команди, які слід використовувати при роботі з файловою системою. Але там ті команди не для вас, в Ruby є вбудовані класи Dir, File та Pathname для роботи з ФС.

Якщо вам потрібно перевірити чи є якийсь додаток в змінній PATH, можна використати MakeMakefile.find_executable. Але цей код згенерує файл з логом в поточній директорії. Щоб уникнути цього, використовуйте наступний код:

require 'mkmf'
MakeMakefile::Logging.instance_variable_set(:@log, File.open(File::NULL, 'w'))
executable = MakeMakefile.find_executable('clear') # або інший, потрібний вам додаток

Кожен раз коли ви показуєте користувачу меню, воно повинно бути розміщено в одному місці. Для цього потрібно кожен очищати екран перед тим, як відображати меню.

В минулому прикладі я шукав утиліту clear саме для цього. Але на Windows аналогічна утиліта називається cls і знайти її вищевказаним способом неможливо, бо вона є частиною command.com.

Але є й гарні новини. Виклик clear на Linux спричиняє друк цих символів: \\e[3J\\e[H\\e[2J. Я та мої знайомі протестували цей же спосіб на Mac та Windows, і він дійсно працює!

Ця послідовність складається з трьох частин:

CLEAR = (ERASE_SCOLLBACK = "\\e[3J") + (CURSOR_HOME = "\\e[H") + (ERASE_DISPLAY = "\\e[2J")

Я рекомендую використовувати готову бібліотеку для цього, а не виводити системні послідовності напряму.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.8K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація