命令行界面应用程序¶
编写命令行界面应用程序(CLI 应用程序)是开发人员最有趣的任务之一。所以,让我们在 Crystal 中构建第一个 CLI 应用程序,享受乐趣吧。
构建 CLI 应用程序时有两个主要主题
输入¶
本主题涵盖所有与以下内容相关的知识
选项¶
传递选项给应用程序是一种非常常见的做法。例如,我们可以运行 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 应用程序有两个选项
-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
的块就会被执行!
我们可以在 官方 API 文档 中了解关于 OptionParser
的所有内容。从那里,只需点击一下,我们就可以找到源代码……这是它不是魔法的真实证明!
现在,让我们运行我们的应用程序。我们有两种方法可以 使用编译器
我们将使用第二种方法
$ crystal run ./help.cr -- -h
Welcome to The Beatles App!
-v, --version Show version
-h, --help Show help
让我们再构建一个很棒的应用程序,它具有以下功能
默认情况下(即没有指定选项),应用程序会显示 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 ...
哦,不!它坏了:我们需要处理无效选项和传递给选项的无效参数!对于这两种情况,OptionParser
类有两个方法:#invalid_option
和 #missing_option
所以,让我们添加这个选项处理程序,并将所有这些 CLI 应用程序合并成一个很棒的 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
请求用户输入¶
有时,我们可能需要用户输入一个值。我们如何读取该值?太简单了!让我们创建一个新的应用程序: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}🎶🎸🥁"
方法 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
具有一些很棒的功能:开箱即用的文件名自动完成;自定义自动完成方法;键绑定,仅举几例。如果我们想尝试它,那么 crystal-lang/crystal-readline shard 将为我们提供一个简单的 API 来使用 Readline
。
另一方面,我们有NCurses
(新 Curses)。这个库允许开发人员在终端中创建图形用户界面。顾名思义,它是名为Curses
库的改进版本,该库最初是为了支持一个名为 Rogue 的基于文本的地下城探险游戏而开发的!正如你所想象的,生态系统中已经有一些 shard 允许我们在 Crystal 中使用 NCurses
!
就这样,我们到达了结尾 😎🎶