跳到內容

命令列介面應用程式

程式設計命令列介面應用程式 (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:它會顯示應用程式的說明。
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 的區塊!

我們可以在 官方 API 文件中閱讀關於 OptionParser 的所有資訊。從那裡,我們只需點擊一下就可以找到原始碼…這證明它不是魔法!

現在,讓我們執行我們的應用程式。我們有兩種方法可以使用 編譯器

  1. 建置應用程式,然後執行它。
  2. 編譯並執行應用程式,所有步驟都在一個命令中完成。

我們將使用第二種方法

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

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

讓我們建置另一個具有以下功能的精彩應用程式

預設情況下(即不給任何選項),應用程式會顯示披頭四樂團的成員名字。但是,如果我們傳遞選項 -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 ...

喔不!它壞掉了:我們需要處理無效選項和給選項的無效參數!對於這兩種情況,OptionParser 類別有兩個方法:#invalid_option#missing_option

因此,讓我們加入這個選項處理器,並將所有這些 CLI 應用程式合併為一個精彩的 CLI 應用程式!

我的所有 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

要求使用者輸入

有時,我們可能需要使用者輸入一個值。我們如何讀取那個值?簡單得很!讓我們建立一個新的應用程式:披頭四樂團會和我們一起唱我們想要的任何短語。當執行應用程式時,它會要求使用者輸入一個短語,然後魔法就會發生!

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。因此,我們必須測試 Nilempty,並針對每種情況自然地採取行動

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}🎶🎸🥁"

輸出

現在,我們將專注於第二個主要主題:我們應用程式的輸出。首先,我們的應用程式已經顯示資訊,但(我認為)我們可以做得更好。讓我們為輸出增加更多生命(即顏色!)。

為了完成這個目標,我們將使用 Colorize 模組。

讓我們建立一個非常簡單的應用程式,顯示具有顏色的字串!我們將在黑色背景上使用黃色字體

yellow_cli.cr
require "colorize"

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

太棒了!這很容易!現在想像一下,將這個字串用作我們的「我的所有 CLI」應用程式的橫幅,如果你嘗試,它很容易

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

對於我們的第二個應用程式,我們將加入一個文字裝飾(在這種情況下為 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)}"

讓我們試試看更新後的應用程式…並聽差異!現在我們有兩個很棒的應用程式了!

您可以在 API 文件中找到可用顏色文字裝飾的列表。

測試

與任何其他應用程式一樣,在某些時候,我們希望為不同的功能撰寫測試

目前,包含每個應用程式邏輯的程式碼始終與 OptionParser 一起執行,也就是說,沒有辦法在不執行整個應用程式的情況下包含該檔案。因此,我們首先需要重構程式碼,將解析選項所需的程式碼與邏輯分開。完成重構後,我們可以開始測試邏輯,並將包含邏輯的檔案包含在我們需要的測試檔案中。我們將此作為讀者的練習。

使用 ReadlineNCurses

如果我們想建立更豐富的 CLI 應用程式,有一些函式庫可以幫助我們。在這裡,我們將提到兩個廣為人知的函式庫:ReadlineNCurses

正如 GNU Readline 函式庫 的文件中所述,Readline 是一個函式庫,為應用程式提供一組函式,讓使用者在輸入時可以編輯命令列。Readline 有一些很棒的功能:開箱即用的檔案名稱自動完成;自訂自動完成方法;按鍵綁定,僅舉幾例。如果我們想嘗試它,那麼 crystal-lang/crystal-readline 這個 shard 將為我們提供一個簡單的 API 來使用 Readline

另一方面,我們有 NCurses (New Curses)。這個函式庫讓開發者可以在終端機中建立圖形化使用者介面。顧名思義,它是名為 Curses 的函式庫的改進版本,該函式庫的開發是為了支援一款名為 Rogue 的文字式地牢探險遊戲!您可以想像,生態系統中已經有一些 shards 可以讓我們在 Crystal 中使用 NCurses

就這樣,我們到達了尾聲 😎🎶