コンテンツにスキップ

コマンドラインインターフェースアプリケーション

コマンドラインインターフェースアプリケーション (CLIアプリケーション) のプログラミングは、開発者が行うことができる最も楽しいタスクの1つです。では、Crystalで最初のCLIアプリケーションを構築してみましょう。

CLIアプリケーションを構築する際には、主に2つのトピックがあります。

入力

このトピックでは、以下のすべてに関連することを説明します。

オプション

アプリケーションにオプションを渡すことは非常に一般的な方法です。たとえば、`crystal -v`を実行すると、Crystalは以下を表示します。

$ crystal -v
Crystal 1.14.0 [dacd97bcc] (2024-10-09)

LLVM: 18.1.6
Default target: x86_64-unknown-linux-gnu

また、`crystal -h`を実行すると、Crystalは受け入れられるすべてのオプションとその使用方法を表示します。

そこで疑問が生じます。**オプションパーサーを実装する必要があるのでしょうか?** その必要はありません。Crystalには`OptionParser`クラスが用意されています。このパーサーを使用してアプリケーションを構築してみましょう。

最初に、CLIアプリケーションには2つのオプションがあります。

  • `-v` / `--version`:アプリケーションのバージョンを表示します。
  • `-h` / `--help`:アプリケーションのヘルプを表示します。
help.cr
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ドキュメントで詳しく読むことができます。そして、そこからソースコード、つまり魔法ではないことの実際の証拠まで、ワンクリックです。

では、アプリケーションを実行してみましょう。 コンパイラを使用するには2つの方法があります。

  1. アプリケーションをビルドする ثم تشغيله.
  2. アプリケーションをコンパイルして実行する、すべて1つのコマンドで行います。

2番目の方法を使用します。

$ crystal run ./help.cr -- -h

Welcome to The Beatles App!
    -v, --version                    Show version
    -h, --help                       Show help

次の機能を備えた、もう1つの*素晴らしい*アプリケーションを構築してみましょう。

デフォルトでは(つまり、オプションが指定されていない場合)、アプリケーションはFab Fourの名前を表示します。ただし、`-t` / `--twist`オプションを渡すと、名前は大文字で表示されます。

twist_and_shout.cr
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`オプションを渡すと、アプリケーションは**オプションのパラメータとして渡された**指定された名前に挨拶します。*

hello_goodbye.cr
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つのメソッドがあります。

では、このオプションハンドラを追加して、これらすべてのCLIアプリケーションを1つの素晴らしいCLIアプリケーションにマージしてみましょう。

All My CLI: 完全なアプリケーション

無効/不足しているオプションの処理と、その他の新しいオプションを追加した最終結果を以下に示します。

all_my_cli.cr
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

ユーザー入力のリクエスト

ユーザーに値を入力してもらう必要がある場合があります。その値をどのように*読み取る*のでしょうか? 簡単です! 新しいアプリケーションを作成してみましょう。Fab Fourは私たちが望むフレーズを一緒に歌ってくれます。アプリケーションを実行すると、ユーザーにフレーズが要求され、魔法が起こります。

let_it_cli.cr
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`を押した場合)が返されます。問題を説明するために、次のようにしてみましょう。ユーザーが入力した内容を大声で歌ってもらいたいとします。

let_it_cli.cr
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`をテストし、それぞれの場合に適切な処理を行う必要があります。

let_it_cli.cr
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`モジュールを使用します。

色付きの文字列を表示する非常にシンプルなアプリケーションを構築してみましょう。黒い背景に黄色のフォントを使用します。

yellow_cli.cr
require "colorize"

puts "#{"The Beatles".colorize(:yellow).on(:black)} App"

素晴らしい! 簡単でしたね! この文字列をAll My CLIアプリケーションのバナーとして使用することを想像してみてください。やってみれば簡単です。

parser.banner = "#{"The Beatles".colorize(:yellow).on(:black)} App"

2番目のアプリケーションでは、*テキストデコレーション*(この場合は`blink`)を追加します。

let_it_cli.cr
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で実行されます。つまり、アプリケーション全体を実行せずにそのファイルを含める方法はありません。そのため、最初にコードをリファクタリングし、オプションの解析に必要なコードとロジックを分離する必要があります。リファクタリングが完了したら、ロジックのテストを開始し、必要なテストファイルにロジックを含むファイルを含めることができます。これは読者への課題として残しておきます。

ReadlineNCursesの使用

よりリッチなCLIアプリケーションを構築したい場合は、役立つライブラリがあります。ここでは、よく知られている2つのライブラリ、ReadlineNCursesを紹介します。

GNU Readlineライブラリのドキュメントに記載されているように、Readlineは、ユーザーが入力時にコマンドラインを編集できるようにするアプリケーションで使用するための一連の関数を提供するライブラリです。 Readlineには、すぐに使えるファイル名自動補完、カスタム自動補完メソッド、キーバインディングなど、いくつかの優れた機能があります。試してみたい場合は、crystal-lang/crystal-readlineシャードがReadlineを使用するための簡単なAPIを提供します。

一方、NCurses(New Curses)があります。このライブラリを使用すると、開発者はターミナルでグラフィカルなユーザーインターフェイスを作成できます。その名前が示すように、これは、Rogue!と呼ばれるテキストベースのダンジョンクロールアドベンチャーゲームをサポートするために開発されたCursesという名前のライブラリの改良版です。ご想像のとおり、CrystalでNCursesを使用できるいくつかのシャードがすでにエコシステムにあります!

これで終わりです😎🎶