命令列介面應用程式¶
程式設計命令列介面應用程式 (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
讓我們建置另一個具有以下功能的精彩應用程式
預設情況下(即不給任何選項),應用程式會顯示披頭四樂團的成員名字。但是,如果我們傳遞選項 -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
要求使用者輸入¶
有時,我們可能需要使用者輸入一個值。我們如何讀取那個值?簡單得很!讓我們建立一個新的應用程式:披頭四樂團會和我們一起唱我們想要的任何短語。當執行應用程式時,它會要求使用者輸入一個短語,然後魔法就會發生!
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"
太棒了!這很容易!現在想像一下,將這個字串用作我們的「我的所有 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
(New Curses)。這個函式庫讓開發者可以在終端機中建立圖形化使用者介面。顧名思義,它是名為 Curses
的函式庫的改進版本,該函式庫的開發是為了支援一款名為 Rogue 的文字式地牢探險遊戲!您可以想像,生態系統中已經有一些 shards 可以讓我們在 Crystal 中使用 NCurses
!
就這樣,我們到達了尾聲 😎🎶