CLI アプリケーション¶
コマンドラインインターフェースアプリケーション (CLI アプリケーション) の開発は、プログラマのタスクの中で最も愉快なものの1つです。それでは、Crystal での最初の CLI の開発を楽しみましょう。
CLI アプリケーションの開発には、主なトピックが2つあります。
入力¶
このトピックでは、次の話題に関連することを扱っていきます。
オプション¶
アプリケーションにオプションを渡すことはとても一般的です。例えばcrystal -v
を実行すると、Crystal は次のように表示します。
$ crystal -v
Crystal 1.3.0 [a3ee70ca0] (2022-01-06)
LLVM: 10.0.0
Default target: x86_64-unknown-linux-gnu
そしてcrystal -h
と実行すると、Crystal は自身のオプションとそれらの利用方法を表示します。
ここで「オプションの解析部分を実装する必要がある?」と疑問を持つことでしょう。その必要はありません。Crystal ではOptionParser
がその機能を提供しています。それではこのパーサーを使ったアプリケーションを作ってみましょう。
まずは次の2つのオプションを持つ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 ドキュメントによって確認できます。そして、そこから1クリックでソースコードを見ることができます。……これが魔法ではないということの証明です。
さて、それではアプリケーションを実際に実行してみましょう。コンパイラの実行方法は2種類あります。
- アプリケーションのビルドをして、それを実行する。
- コンパイルとアプリケーションの実行を、1度のコマンドでまとめて行う。
今回は2番目の方法でいきたいと思います。
$ crystal run ./help.cr -- -h
Welcome to The Beatles App!
-v, --version Show version
-h, --help Show help
続けて、こんな機能を持つ_ファビュラスな_アプリケーションを作ってみましょう:
デフォルトでは (オプションが与えられなかったときに) The Fab Four のメンバーを表示します。しかし、-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 ...
なんてことでしょう。これは壊れていますね。無効なオプションと無効なパラメーターが渡されたときの処理をする必要するがあります。2つの状況に応じて、OptionParser
クラスは#invalid_option
と#missing_option
という2つメソッドを持っています。
それでは、これらのオプションハンドラーを追加して、すべてを1つのすばらしいCLIアプリケーションにマージしましょう。
All My CLI: 完成した 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
ユーザーへの入力の要求¶
しばしばユーザーに値を入力してもらいたい場合があります。どのようにして値を_読む_のでしょうか? 簡単です!The Fab Four が望むフレーズを唄ってくれる、というアプリケーションを作ってみましょう。このアプリケーションを実行すると、ユーザーにフレーズを要求して、そして魔法が起こります!
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}🎶🎸🥁"
The method gets
will pause the execution of the application until the user finishes entering the input (pressing the Enter
key).
ユーザーが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
もしくは""
(空文字列) かをチェックして、自然に動作するようにしましょう。
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}🎶🎸🥁"
出力¶
ここからは、アプリケーションの出力という、2つ目のトピックに取りかかっていきます。 はじめに、アプリケーションは現時点でも情報を表示してはいるけど、そこまで良い表示だとは言えません。せっかくなので出力を 色付け してみましょう。
これを達成するためにColorize
モジュールを使いたいと思います。
色付いた文字列を表示する、単純なアプリケーションを作ってみましょう。黒の背景に黄色のフォントを使用します:
require "colorize"
puts "#{"The Beatles".colorize(:yellow).on(:black)} App"
いい感じですね。簡単でしょう。イマジン (想像) してみてください、All My CLI アプリケーションのバナーにこの文字列を使うことを。ほら、簡単でしょう? (it's easy if you try?)
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)}"
生まれ変わったアプリケーションを試してみてください……そして、違いを_聴き取って_ください。 今、私たちは2つのファビュラスなアプリケーションを実装したのです。
利用できる色やテキストの装飾の一覧はAPI ドキュメントで確認できます。
テスト¶
他のアプリケーションと同じように、ある時点で、さまざまな機能のテストを書きたいと思うでしょう。
現時点ではアプリケーションの各ロジックはOptionParser
の内で実行されています。つまり、アプリケーション全体を実行することなしに、部分的にファイルを取り込むことができないのです。よって、まずはじめにオプションの解析部分と実際のロジックを分離するリファクタリングを行う必要があります。リファクタリングが完了したら、ロジックのテストを開始し、必要なテストファイルにロジックを含むファイルを含めることができます。これは読者の課題としましょう。これを読者の課題とします。.
Readline
とNCurses
の利用¶
よりリッチなCLI アプリケーションを構築しようと思ったとき、これらのライブラリが助けになります。Readline
とNCurses
という、2つのよく知られたライブラリです。
GNU Readline Libraryというドキュメントで述べられているように、Readline
はユーザーに対してコマンドライン編集機能を提供します。
Readline
は素晴らしく強力な機能があります。ボックス外でのファイル名の自動補完、自動補完方法のカスタマイズ、キーバインド変更というのはほんの一例です。それらの機能を使いたいのであればcrystal-lang/crystal-readline shardがReadline
を簡単に扱える API を提供しています。
続いて、NCurses
(New Curses) の紹介です。このライブラリは端末で_グラフィカルな_ユーザーインターフェースを開発することを可能にします。その名前が暗に示すように、これはCurses
というライブラリの改良版です。Curses
は Rouge というテキストベースのダンジョン探索アドベンチャーゲームのために開発されました。
NCurses
を Crystal から扱える shardは2つほど存在しています。
これでこの文章はおしまいです 😎🎶