Приложение командной строки
Разработка приложений командной строки (CLI-приложений) — одно из самых увлекательных занятий для разработчика. Так что давайте повеселимся и создадим наше первое CLI-приложение на Crystal.
При создании CLI-приложения есть две основные темы:
Ввод
В этой теме рассматриваются все аспекты, связанные с:
Параметры
Передавать приложению параметры — обычная практика. Например, мы можем выполнить crystal -v, и Crystal выведет:
$ crystal -v
Crystal 1.2.2 [6529d725a] (2021-11-10)
LLVM: 10.0.0
Default target: x86_64-unknown-linux-gnu
а если выполнить crystal -h, Crystal покажет все допустимые параметры и способы их использования.
Итак, возникает вопрос: нужно ли нам реализовывать анализатор параметров? Нет, Crystal уже позаботился об этом с помощью класса OptionParser. Давайте создадим приложение, используя этот анализатор!
Вначале у нашего CLI-приложения есть два параметра:
-
-v/--version: отображает версию приложения. -
-h/--help: отображает справку по приложению.
require "option_parser"
OptionParser.parse do |parser|
parser.banner = "Welcome to The Beatles App!"
parser.on "-v", "--version", "Show version" do
puts "version 1.0"
exit
end
parser.on "-h", "--help", "Show help" do
puts parser
exit
end
end
Как же всё это работает? Ну… магия! Нет, это не совсем магия! Просто Crystal упрощает нам жизнь. При запуске приложения выполняется блок, переданный в OptionParser#parse. В этом блоке мы определяем все параметры. После выполнения блока анализатор начинает обрабатывать аргументы, переданные приложению, и пытается сопоставить каждый из них с определёнными нами параметрами. Если параметр совпадает, выполняется блок, переданный в parser#on!
Всё о OptionParser можно прочитать в официальной документации API. А оттуда до исходного кода всего один щелчок мышью… вот настоящее доказательство того, что это не магия!
Теперь запустим наше приложение. Есть два способа сделать это с помощью компилятора:
- Собрать приложение, а затем запустить его.
- Скомпилировать и запустить приложение одной командой.
Мы воспользуемся вторым способом:
$ crystal run ./help.cr -- -h
Welcome to The Beatles App!
-v, --version Show version
-h, --help Show help
Давайте создадим ещё одно замечательное приложение со следующей возможностью:
По умолчанию (то есть если параметры не указаны) приложение будет отображать имена участников Великолепной четвёрки. Но если передать параметр -t / --twist, оно выведет имена заглавными буквами:
require "option_parser"
the_beatles = [
"John Lennon",
"Paul McCartney",
"George Harrison",
"Ringo Starr",
]
shout = false
option_parser = OptionParser.parse do |parser|
parser.banner = "Welcome to The Beatles App!"
parser.on "-v", "--version", "Show version" do
puts "version 1.0"
exit
end
parser.on "-h", "--help", "Show help" do
puts parser
exit
end
parser.on "-t", "--twist", "Twist and SHOUT" do
shout = true
end
end
members = the_beatles
members = the_beatles.map &.upcase if shout
puts ""
puts "Group members:"
puts "=============="
members.each do |member|
puts member
end
Запуск приложения с параметром -t выведет:
$ crystal run ./twist_and_shout.cr -- -t
Group members:
==============
JOHN LENNON
PAUL MCCARTNEY
GEORGE HARRISON
RINGO STARR
Параметры со значениями
Создадим ещё одно приложение: если передать параметр -g / --goodbye_hello, приложение поприветствует указанное имя, переданное в качестве значения параметра.
require "option_parser"
the_beatles = [
"John Lennon",
"Paul McCartney",
"George Harrison",
"Ringo Starr",
]
say_hi_to = ""
option_parser = OptionParser.parse do |parser|
parser.banner = "Welcome to The Beatles App!"
parser.on "-v", "--version", "Show version" do
puts "version 1.0"
exit
end
parser.on "-h", "--help", "Show help" do
puts parser
exit
end
parser.on "-g NAME", "--goodbye_hello=NAME", "Say hello to whoever you want" do |name|
say_hi_to = name
end
end
unless say_hi_to.empty?
puts ""
puts "You say goodbye, and #{the_beatles.sample} says hello to #{say_hi_to}!"
end
В этом случае блок получает параметр, представляющий значение, переданное параметру приложения.
Давайте попробуем!
$ crystal run ./hello_goodbye.cr -- -g "Penny Lane"
You say goodbye, and Ringo Starr says hello to Penny Lane!
Отлично! Эти приложения выглядят потрясающе! Но что произойдёт, если передать параметр, который не объявлен? Например, -n
$ crystal run ./hello_goodbye.cr -- -n
Unhandled exception: Invalid option: -n (OptionParser::InvalidOption)
from ...
О нет! Всё сломалось: нам нужно обрабатывать недопустимые параметры и недопустимые значения, переданные параметру! Для этих двух ситуаций в классе OptionParser есть два метода: #invalid_option и #missing_option
Добавим обработчик параметров и объединим все эти CLI-приложения в одно замечательное CLI-приложение!
All My CLI: полное приложение!
Вот итоговый результат с обработкой недопустимых и отсутствующих параметров, а также с несколькими новыми параметрами:
require "option_parser"
the_beatles = [
"John Lennon",
"Paul McCartney",
"George Harrison",
"Ringo Starr",
]
shout = false
say_hi_to = ""
strawberry = false
option_parser = OptionParser.parse do |parser|
parser.banner = "Welcome to The Beatles App!"
parser.on "-v", "--version", "Show version" do
puts "version 1.0"
exit
end
parser.on "-h", "--help", "Show help" do
puts parser
exit
end
parser.on "-t", "--twist", "Twist and SHOUT" do
shout = true
end
parser.on "-g NAME", "--goodbye_hello=NAME", "Say hello to whoever you want" do |name|
say_hi_to = name
end
parser.on "-r", "--random_goodbye_hello", "Say hello to one random member" do
say_hi_to = the_beatles.sample
end
parser.on "-s", "--strawberry", "Strawberry fields forever mode ON" do
strawberry = true
end
parser.missing_option do |option_flag|
STDERR.puts "ERROR: #{option_flag} is missing something."
STDERR.puts ""
STDERR.puts parser
exit(1)
end
parser.invalid_option do |option_flag|
STDERR.puts "ERROR: #{option_flag} is not a valid option."
STDERR.puts parser
exit(1)
end
end
members = the_beatles
members = the_beatles.map &.upcase if shout
puts "Strawberry fields forever mode ON" if strawberry
puts ""
puts "Group members:"
puts "=============="
members.each do |member|
puts "#{strawberry ? "🍓" : "-"} #{member}"
end
unless say_hi_to.empty?
puts ""
puts "You say goodbye, and I say hello to #{say_hi_to}!"
end
Запрос пользовательского ввода
Иногда нам может понадобиться, чтобы пользователь ввёл значение. Как его прочитать? Проще простого! Создадим новое приложение: участники Великолепной четвёрки будут петь вместе с нами любую фразу. При запуске приложение запросит у пользователя фразу, и произойдёт магия!
puts "Welcome to The Beatles Sing-Along version 1.0!"
puts "Enter a phrase you want The Beatles to sing"
print "> "
user_input = gets
puts "The Beatles are singing: 🎵#{user_input}🎶🎸🥁"
Метод gets приостанавливает выполнение приложения, пока пользователь не завершит ввод (нажав клавишу Enter). Когда пользователь нажмёт Enter, выполнение продолжится, а user_input будет содержать введённое пользователем значение.
Но что, если пользователь не введёт никакого значения? В этом случае мы получим пустую строку (если пользователь просто нажмёт Enter) или, возможно, значение Nil (если поток ввода закрыт, например, нажатием Ctrl+D). Чтобы показать проблему, попробуем сделать следующее: мы хотим, чтобы введённые пользователем данные были исполнены громко:
puts "Welcome to The Beatles Sing-Along version 1.0!"
puts "Enter a phrase you want The Beatles to sing"
print "> "
user_input = gets
puts "The Beatles are singing: 🎵#{user_input.upcase}🎶🎸🥁"
При запуске примера Crystal ответит:
$ crystal run ./let_it_cli.cr
Showing last frame. Use --error-trace for full trace.
In let_it_cli.cr:5:46
5 | puts "The Beatles are singing: 🎵#{user_input.upper_case}
^---------
Error: undefined method 'upper_case' for Nil (compile-time type is (String | Nil))
А! Надо было подумать заранее: тип пользовательского ввода — это объединённый тип String | Nil. Поэтому нужно проверить, не является ли значение Nil или empty, и обработать каждый случай соответствующим образом:
puts "Welcome to The Beatles Sing-Along version 1.0!"
puts "Enter a phrase you want The Beatles to sing"
print "> "
user_input = gets
exit if user_input.nil? # Ctrl+D
default_lyrics = "Na, na, na, na-na-na na" \
" / " \
"Na-na-na na, hey Jude"
lyrics = user_input.presence || default_lyrics
puts "The Beatles are singing: 🎵#{lyrics.upcase}🎶🎸🥁"
Вывод
Теперь сосредоточимся на второй основной теме: выводе нашего приложения. Для начала отметим, что наши приложения уже отображают информацию, но, думаю, можно сделать это лучше. Добавим выводу немного жизни (то есть цвета!).
Для этого мы воспользуемся модулем Colorize.
Создадим очень простое приложение, которое выводит цветную строку! Используем жёлтый шрифт на чёрном фоне:
require "colorize"
puts "#{"The Beatles".colorize(:yellow).on(:black)} App"
Отлично! Это было просто! Теперь представьте, что мы используем эту строку как баннер для нашего приложения All My CLI. Попробуйте — это легко:
parser.banner = "#{"The Beatles".colorize(:yellow).on(:black)} App"
Во втором приложении добавим оформление текста (blinkв данном случае):
require "colorize"
puts "Welcome to The Beatles Sing-Along version 1.0!"
puts "Enter a phrase you want The Beatles to sing"
print "> "
user_input = gets
exit if user_input.nil? # Ctrl+D
default_lyrics = "Na, na, na, na-na-na na" \
" / " \
"Na-na-na na, hey Jude"
lyrics = user_input.presence || default_lyrics
puts "The Beatles are singing: #{"🎵#{lyrics}🎶🎸🥁".colorize.mode(:blink)}"
Давайте попробуем обновлённое приложение… и услышим разницу!! Теперь у нас есть два замечательных приложения!!
Список доступных цветов и вариантов оформления текста можно найти в документации API.
Тестирование
Как и для любого другого приложения, рано или поздно нам захочется написать тесты для различных возможностей.
Сейчас код, содержащий логику каждого приложения, всегда выполняется с помощью OptionParser, то есть подключить этот файл, не запустив всё приложение целиком, невозможно. Поэтому сначала нам нужно реорганизовать код, отделив код для разбора параметров от логики приложения. После реорганизации мы сможем начать тестировать логику и подключать файл с ней в нужные файлы тестов. Оставим это упражнение читателю.
Использование Readline и NCurses
Если мы хотим создавать более функциональные CLI-приложения, нам могут помочь библиотеки. Здесь мы назовём две известные библиотеки: Readline и NCurses.
Как сказано в документации библиотеки GNU Readline, Readline предоставляет набор функций для приложений, позволяющих пользователям редактировать вводимые команды. У Readline есть несколько полезных возможностей: автодополнение имён файлов из коробки, пользовательский метод автодополнения, привязка клавиш — и это лишь некоторые из них. Если вы хотите попробовать эту библиотеку, shard crystal-lang/crystal-readline предоставит удобный API для работы с Readline.
С другой стороны, есть NCurses (New Curses). Эта библиотека позволяет разработчикам создавать графические пользовательские интерфейсы в терминале. Как следует из названия, это улучшенная версия библиотеки Curses, разработанной для поддержки текстовой приключенческой игры в жанре dungeon crawl под названием Rogue! Как вы можете догадаться, в экосистеме уже есть несколько shard-пакетов, позволяющих использовать NCurses в Crystal!
И вот мы подошли к концу 😎🎶
To the extent possible under law, the persons who contributed to this workhave waived
all copyright and related or neighboring rights to this workby associating CC0 with it.
https://crystal-lang.org/reference/1.2/getting_started/cli.html