返回文章列表

Rust 命令列程式開發

本文介紹如何使用 Rust 開發命令列程式,涵蓋從專案建立、引數解析、錯誤處理到彩色輸出和檔案讀取等方面,並以 catsay 工具的開發為例,逐步講解如何使用 Cargo、StructOpt 和 colored 等套件,以及如何利用 Rust 的錯誤處理機制和 `?` 運算元簡化程式碼,提升程式碼的健壯性和可讀性。

Rust 命令列程式

Rust 適合開發命令列程式,crates.io 上豐富的函式庫能滿足各種功能需求。其高效能和安全性則能避免效能瓶頸和常見錯誤,相較於 Python 或 Ruby 等指令碼語言更具優勢。Rust 程式編譯成單一小型二進位檔案,方便分發,使用者無需安裝額外執行環境。本文將以 catsay 工具開發為例,示範如何使用 Rust 建立命令列程式,包含引數解析、錯誤處理、彩色輸出和檔案讀取等技巧。catsay 類別似 cowsay,但顯示的是貓咪圖案。我們將使用 Cargo 建立專案,並使用 StructOpt 解析命令列引數,以及 colored 套件實作彩色輸出。此外,我們也會探討如何從檔案讀取貓咪圖案,以及如何使用 ? 運算元簡化錯誤處理流程,並使用 failure 套件提供更友善的錯誤訊息。最後,我們將說明如何處理管線功能,並透過設定 NO_COLOR 環境變數來停用顏色輸出,以確保程式與其他命令列工具的相容性。

第二章:建立命令列程式

簡介

命令列程式,也稱為CLI(命令列介面),是Rust最自然的應用之一。當您編譯第一個Hello World程式時,您正在建立一個命令列程式。典型的命令列程式接受引數、旗標,有時還接受標準輸入,然後執行其主要演算法並輸出到標準輸出或檔案。所有這些操作都得到了Rust標準函式庫和crates.io上的第三方crate的良好支援。

在Rust中建立CLI有幾個優勢。首先,crates.io上豐富的函式庫集合使您能夠實作許多所需的功能。其次,其高效能和安全性保證讓您能夠減輕許多效能瓶頸和錯誤,相較於其他流行的指令碼語言,如Python或Ruby。最後,Rust程式可以被編譯成單一的小型二進位檔案,包含平台特定的機器碼,以便於分發,因此使用者不需要在其系統上安裝語言執行環境。這方面的例子包括ripgrep專案,它是一個類別似於GNU grep、ack或The Silver Searcher的行導向搜尋工具。Ripgrep具有卓越的效能,在許多情況下甚至優於根據C的GNU grep。

使用 Rust 開發命令列程式:開發 catsay 工具

在開發命令列工具時,Rust 提供了一個強大且安全的環境。本章節將透過建立一個名為 catsay 的工具來展示如何使用 Rust 開發命令列程式。catsay 是一個類別似於經典工具 cowsay 的程式,但它會顯示一隻貓而不是牛。

建立二進位制專案

要開始,首先需要建立一個新的 Rust 專案。使用 Cargo(Rust 的套件管理器)可以輕鬆完成這項任務。執行以下命令:

$ cargo new --bin catsay

這將建立一個名為 catsay 的資料夾,其中包含基本的專案結構:

catsay
|-- Cargo.toml
+-- src
    +-- main.rs

main.rs 檔案中已經包含了一個簡單的 “Hello World” 程式範例。要執行這個範例,可以在 catsay 資料夾中執行:

$ cargo run

讀取命令列引數

第一步是實作讀取命令列引數的功能,以便將傳遞給程式的字串顯示在貓的對話泡泡中。可以使用 std::env::args() 函式來讀取命令列引數,如下所示:

fn main() {
    let message = std::env::args().nth(1)
        .expect("Missing the message. Usage: catsay <message>");
    println!("{}", message);
    println!(" \\");
    println!(" \\");
    println!(" /\\_/\\");
    println!(" ( o o )");
    println!(" =( I )=");
}

內容解密:

  1. std::env::args():傳回一個迭代器,包含傳遞給程式的命令列引數。
  2. .nth(1):取得第一個引數(索引 0 是程式名稱本身)。
  3. .expect():如果引數不存在,則顯示錯誤訊息並離開程式。
  4. println!():用於列印訊息和貓的 ASCII 影像。

處理複雜的命令列引數

對於具有多個選項的程式,手動解析命令列引數會變得非常繁瑣。這時可以使用 clapStructOpt 這樣的函式庫來簡化這個過程。StructOpt 結合了 clap 和自定義派生(custom derive)的功能,使得定義和解析命令列引數變得更加容易。

使用 StructOpt

首先需要在 Cargo.toml 中新增 StructOpt 的依賴:

[dependencies]
structopt = "0.3.22"

然後,可以定義一個結構體來儲存命令列引數,並使用 #[derive(StructOpt)] 自動生成解析器:

use structopt::StructOpt;

#[derive(StructOpt)]
struct CatsayOpt {
    #[structopt(short = "d", long = "dead")]
    dead: bool,
    #[structopt()]
    message: String,
}

fn main() {
    let opt = CatsayOpt::from_args();
    // 處理選項和訊息
}

內容解密:

  1. #[derive(StructOpt)]:自動為 CatsayOpt 結構體生成命令列引數解析器。
  2. #[structopt(short = "d", long = "dead")]:定義一個名為 dead 的選項,可以使用 -d--dead 來指定。
  3. message: String:定義一個名為 message 的位置引數,用於儲存要顯示的訊息。
  • 錯誤處理:增強程式的錯誤處理能力,確保在遇到無效輸入或執行錯誤時能夠提供有用的錯誤訊息。
  • 測試:編寫單元測試和整合測試,以確保程式的正確性和穩定性。
  • 發布:將 catsay 工具發布到 crates.io,方便其他開發者使用。

本章節展示瞭如何使用 Rust 和相關函式庫建立一個功能豐富的命令列工具。透過遵循最佳實踐和利用 Rust 生態系統中的強大工具,開發者可以高效地構建可靠且高效的命令列應用程式。

使用 StructOpt 解析命令列引數

在開發命令列程式時,解析命令列引數是一項基本且重要的功能。Rust 的 StructOpt 函式庫提供了一種宣告式的方式來定義和解析命令列引數。在本章節中,我們將探討如何使用 StructOpt 來解析命令列引數,並實作一個簡單的 catsay 程式。

新增 StructOpt 依賴

首先,我們需要在 Cargo.toml 檔案中新增 StructOpt 的依賴:

[package]
name = "catsay"
version = "0.1.0"
authors = ["Shing Lyu <[email protected]>"]
edition = "2018"

[dependencies]
structopt = "0.2.15"

使用 StructOpt 解析命令列引數

接下來,我們將修改 main.rs 檔案以使用 StructOpt 解析命令列引數。我們定義了一個名為 Options 的結構體,並使用 #[derive(StructOpt)] 屬性自動實作 StructOpt trait。

extern crate structopt;
use structopt::StructOpt;

#[derive(StructOpt)]
struct Options {
    /// What does the cat say?
    #[structopt(default_value = "Meow!")]
    message: String,
}

fn main() {
    let options = Options::from_args();
    let message = options.message;
    println!("{}", message);
    // ... 列印貓咪圖案
}

內容解密:

  1. #[derive(StructOpt)] 自動為 Options 結構體實作 StructOpt trait,使其能夠解析命令列引數。
  2. message: String 定義了一個名為 message 的字串欄位,用於儲存命令列引數的值。
  3. #[structopt(default_value = "Meow!")] 設定了 message 欄位的預設值為 “Meow!"。
  4. let options = Options::from_args(); 解析命令列引數並將其儲存在 options 變數中。
  5. let message = options.message;options 變數中取得 message 欄位的值。

新增二元旗標(Binary Flag)

我們可以輕鬆地新增一個名為 --dead 的二元旗標,以改變貓咪的眼睛表情。

#[derive(StructOpt)]
struct Options {
    message: String,
    #[structopt(short = "d", long = "dead")]
    /// Make the cat appear dead
    dead: bool,
}

內容解密:

  1. dead: bool 定義了一個名為 dead 的布林欄位,用於儲存 --dead 旗標的值。
  2. #[structopt(short = "d", long = "dead")] 設定了 --dead 旗標的短名稱為 -d,長名稱為 --dead

根據旗標值改變貓咪眼睛表情

我們可以根據 --dead 旗標的值來改變貓咪的眼睛表情。

let eye = if options.dead { "x" } else { "o" };
println!(" \\");
println!(" \\");
println!(" /\\_/\\");
println!(" ( {eye} {eye} )", eye=eye);
println!(" =( I )=");

內容解密:

  1. let eye = if options.dead { "x" } else { "o" }; 根據 options.dead 的值決定眼睛表情。
  2. println!(" ( {eye} {eye} )", eye=eye); 列印貓咪的眼睛表情。

列印錯誤訊息到 STDERR

當使用者輸入無效的命令列引數時,我們可以將錯誤訊息列印到 STDERR。

if message.to_lowercase() == "woof" {
    eprintln!("A cat shouldn't bark like a dog.")
}

內容解密:

  1. eprintln! 將錯誤訊息列印到 STDERR。
  2. message.to_lowercase() == "woof" 檢查輸入的訊息是否為 “woof”,如果是,則列印錯誤訊息。

為 Catsay 新增顏色與檔案讀取功能

在開發命令列程式時,顏色的使用可以提升使用者的體驗,而讀取外部檔案則是許多命令列工具的基本功能。本章節將介紹如何使用 colored 套件為 Catsay 新增顏色,以及如何實作讀取外部檔案的功能。

使用 Colored 套件新增顏色

現代的終端機(或終端模擬器)通常都支援彩色輸出。為了讓 Catsay 的輸出更加豐富多彩,我們將使用 colored 套件。首先,需要在 Cargo.toml 檔案中新增 colored 套件:

[dependencies]
colored = "1.7.0"

接著,在 main.rs 檔案中引入 colored 套件並使用其提供的功能:

extern crate colored;
use colored::*;

fn main() {
    // ...
    println!("{}", message.bright_yellow().underline().on_purple());
    println!(" \\");
    println!(" \\");
    println!(" /\\_/\\");
    println!(" ( {eye} {eye} )", eye=eye.red().bold());
    println!(" =( I )=");
}

內容解密:

  1. extern crate colored;:宣告使用 colored 外部套件。
  2. use colored::*;:將 colored 套件中的所有專案引入當前名稱空間,方便直接使用其提供的函式。
  3. message.bright_yellow().underline().on_purple():這行程式碼將訊息文字設定為亮黃色、帶有底線,並且背景為紫色。這展示了 colored 套件如何鏈式呼叫多個樣式函式來改變文字的外觀。
  4. eye.red().bold():將貓的眼睛設定為紅色並且加粗,進一步豐富輸出的視覺效果。

colored 套件定義了一個 Colorize 特性(trait),該特性為 &strString 型別實作了多種鏈式呼叫的色彩和樣式函式,包括文字顏色、背景顏色以及各種文字樣式,如粗體、底線等。

從檔案讀取貓圖案

許多命令列應用程式都支援從外部檔案讀取內容,Cowsay 就有一個 -f 選項允許使用者指定自定義的牛圖案檔案。本文將實作一個簡化的版本,以展示如何在 Rust 中讀取檔案。

首先,需要在 Options 結構體中新增一個新的欄位來處理檔案路徑:

#[derive(StructOpt)]
struct Options {
    // ...
    #[structopt(short = "f", long = "file", parse(from_os_str))]
    /// Load the cat picture from the specified file
    catfile: Option<std::path::PathBuf>,
}

內容解密:

  1. #[structopt(short = "f", long = "file", parse(from_os_str))]:這行屬性宏定義了一個新的命令列選項 -f--file,用於指定貓圖案檔案的路徑。parse(from_os_str) 表示該選項的值將被解析為作業系統的字串(OsStr),這使得程式能夠正確處理不同作業系統上的檔案路徑。
  2. catfile: Option<std::path::PathBuf>catfile 欄位被包裝在 Option 中,表示它是一個可選的引數。如果沒有提供該引數,其值將是 None。使用 PathBuf 而不是原始字串,可以更穩健地處理檔案路徑,因為它能夠隱藏不同作業系統之間路徑表示的差異。

接著,在 main 函式中根據 catfile 選項的值來決定是否讀取外部檔案:

let options = Options::from_args();
// ...
let eye = if options.dead { "x" } else { "o" };
println!("{}", message);
match &options.catfile {
    Some(path) => {
        let cat_template = std::fs::read_to_string(path)
            .expect(&format!("could not read file {:?}", path));
        let cat_picture = cat_template.replace("{eye}", eye);
        println!("{}", &cat_picture);
    },
    None => {
        // ... 列印預設的貓圖案
    }
}

內容解密:

  1. match &options.catfile:使用模式匹配檢查 catfile 欄位的值。如果是 Some(path),則嘗試讀取該檔案;如果是 None,則沿用之前的邏輯列印預設的貓圖案。
  2. std::fs::read_to_string(path):讀取指定路徑的檔案內容到字串。如果讀取失敗,則程式會 panic 並顯示錯誤訊息。
  3. cat_template.replace("{eye}", eye):將貓圖案範本中的 {eye} 佔位符替換為實際的眼睛字元(“o” 或 “x”)。由於 format!() 需要在編譯期知道格式化字串,而檔案內容是在執行期讀取的,因此這裡使用 String::replace() 方法來進行字串替換。

更完善的錯誤處理

到目前為止,我們一直使用 unwrap()expect() 方法來處理可能失敗的操作,如讀取檔案。當這些操作失敗時,程式會直接當機並觸發 panic!()。為了提供更好的使用者經驗,我們應該改用 Rust 的 ? 運算元來處理錯誤。

let cat_template = std::fs::read_to_string(path)?;

內容解密:

  1. std::fs::read_to_string(path)?:使用 ? 運算元來傳播錯誤。如果 read_to_string 失敗,函式會立即傳回一個包含錯誤的 Result

這樣不僅能夠提供更友善的錯誤訊息,還能夠讓程式在遇到錯誤時優雅地離開,而不是直接當機。

使用 ? 運算元處理錯誤

在 Rust 程式設計中,錯誤處理是非常重要的一環。透過使用 ? 運算元,可以簡化錯誤處理的過程,使程式碼更加簡潔易讀。

? 運算元的基本用法

當我們在函式中使用 std::fs::read_to_string(path) 來讀取檔案時,如果發生錯誤,傳統的做法是使用 match 陳述式來處理 Result 型別傳回值。但是,使用 ? 運算元可以達到相同的效果,並且使程式碼更加簡潔。

let cat_template = std::fs::read_to_string(path)?;

內容解密:

  • std::fs::read_to_string(path)? 這行程式碼會嘗試讀取指定路徑的檔案內容。
  • 如果讀取成功,? 運算元會將 Ok 中的值解封裝並指定給 cat_template
  • 如果讀取失敗,? 運算元會立即傳回 Err,並且函式會提前結束。

修改 main 函式的簽名

為了使用 ? 運算元,我們需要修改 main 函式的簽名,使其傳回 Result 型別。

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // ...
    std::fs::read_to_string(path)?;
    // ...
    Ok(())
}

內容解密:

  • fn main() -> Result<(), Box<dyn std::error::Error>> 表示 main 函式可能會傳回一個錯誤。
  • Box<dyn std::error::Error> 是一個 trait 物件,表示任何實作了 std::error::Error trait 的型別都可以被傳回。
  • 在函式的最後,我們需要傳回 Ok(()) 以滿足函式簽名的要求。

使用 failure 套件提供更友善的錯誤訊息

預設的錯誤訊息可能不夠友善,因此我們可以使用 failure 套件來提供更詳細的錯誤訊息。

[dependencies]
structopt = "0.2.15"
colored = "1.7.0"
failure = "0.1.5"
exitfailure = "0.5.1"

內容解密:

  • Cargo.toml 中新增 failureexitfailure 套件作為依賴。
  • failure 套件提供了更靈活的錯誤處理機制。

使用 with_context 提供錯誤上下文

透過 failure::ResultExt trait,我們可以使用 with_context 方法為錯誤新增上下文訊息。

use failure::ResultExt;
use exitfailure::ExitFailure;

fn main() -> Result<(), ExitFailure> {
    // ...
    std::fs::read_to_string(path)
        .with_context(|_| format!("could not read file {:?}", path))?;
    // ...
    Ok(())
}

內容解密:

  • .with_context(|_| format!("could not read file {:?}", path)) 為讀取檔案錯誤增加了上下文訊息。
  • 當錯誤發生時,會顯示自定義的錯誤訊息以及原始錯誤訊息。

將輸出導向其他命令

在 UNIX-like 系統中,管線(piping)是一種強大的功能。為了使我們的程式能夠與其他命令列工具協同工作,我們需要正確處理輸入輸出格式。

停用顏色輸出以支援管線功能

當我們將輸出導向到檔案或其他命令時,ANSI 顏色程式碼可能會引起問題。透過設定 NO_COLOR 環境變數,可以停用顏色輸出。

NO_COLOR=1 cargo run

內容解密:

  • 設定 NO_COLOR=1 環境變數可以停用輸出中的顏色程式碼。
  • 這樣可以使輸出的內容更加乾淨,適合導向到其他命令或檔案中。