返回文章列表

Rust 命令列程式開發實務

本文探討 Rust 命令列程式開發的實務技巧,涵蓋 Cargo 專案建置、命令列引數處理、錯誤訊息輸出、以及如何利用 Clap 函式庫簡化引數解析流程,並使用 colored crate 增添程式輸出色彩,提升使用者經驗。同時,文章以 Catsay 專案為例,逐步示範如何建構一個功能完善的命令列程式。

Rust 命令列程式

Rust 非常適合開發命令列程式,其效能優勢、安全性以及便捷的跨平台編譯能力,使其成為建構 CLI 的理想選擇。crates.io 上豐富的函式庫生態,更能加速開發流程。本文將以 Catsay 專案為例,示範如何使用 Rust 建構一個功能完善的命令列程式,包含引數處理、錯誤處理,以及程式碼封裝和發布等環節。首先,我們會使用 Cargo 建立專案架構,接著利用 std::env::args 函式讀取命令列引數,並逐步引導讀者使用 Clap 函式庫簡化引數解析的複雜度,讓程式碼更簡潔易懂。同時,我們也會示範如何將錯誤訊息輸出到 STDERR,以及如何使用 colored crate 為程式輸出增添色彩,提升使用者經驗。

本文不適合的物件

本文可能不適合以下讀者:

  • 想要學習 Rust 程式語言本身的讀者;
  • 想要深入研究某一特定領域的讀者;
  • 尋找最具實驗性和前沿性的 Rust 使用案例的讀者。

本文不是 Rust 程式語言本身的課程,也不是透過範例來教授 Rust 語法。我們將專注於應用程式本身及其領域知識,假設你已經瞭解 Rust 的語法和語言特性(儘管我們會在需要時回顧更進階的功能)。有很多優秀的 Rust 書籍,例如 Steve Klabnik 和 Carol Nichols 的《The Rust Programming Language》。你也可以在官方網站的 Learn Rust 部分找到線上書籍、互動式教學和影片。

選擇函式庫的標準

儘管 Rust 已經被用於許多生產級專案,但它仍然是一種相對年輕的語言,創新不斷。這意味著有時選擇每個章節中使用的函式庫或框架可能是一個挑戰。幾乎每個領域都有實驗性的純 Rust 實作,以及同樣多的 Rust 繫結到其他語言中的流行函式庫(尤其是在 C 和 C++ 中)。在某些領域,已經出現了明確的長官者,某個特定的函式庫成為了事實上的標準。在其他領域,存在許多概念驗證套件,但沒有明確的贏家。Rust 的早期採用者通常是冒險的開發者;他們對函式庫中的粗糙邊緣感到舒適,並找到解決方法(Rust 生態系統使得下載和檢查任何所用函式庫的原始碼變得容易)。一些函式庫專注於創新和實驗,而其他函式庫則重視穩定性和生產就緒性。在本文中,我們試圖展示每個領域的核心概念,以及它們如何轉化為慣用的 Rust API。因此,我們根據以下標準選擇所使用的函式庫。

純 Rust

我們通常試圖找到純粹用 Rust 編寫的函式庫。Rust 的 FFI(外部函式介面)允許你從 Rust 中呼叫現有的 C 函式庫(以及許多其他語言)。這反過來意味著,在 Rust 中快速構建應用程式的最簡單方法通常是利用其他語言中的現有函式庫。這些函式庫通常是為其他語言設計的,因此用 Rust 包裝它們會導致奇怪且非慣用的 Rust API。如果有純粹的 Rust 函式庫或使用現有技術但從頭開始用 Rust 構建的函式庫,我們傾向於選擇這些。

成熟度

然而,並非每個純粹的 Rust 函式庫都非常成熟。因為許多 Rust 函式庫是從零開始構建的,開發人員經常嘗試使用最新的技術,這可能意味著架構和 API 設計非常實驗性且頻繁變化。一些函式庫在早期顯示出巨大的潛力,但隨後開發速度減慢,最終進入維護模式或被放棄。我們的目標是構建有用的軟體,而不是嘗試使用令人興奮的技術然後丟棄程式碼。因此,我們需要務實地選擇足夠成熟且使用廣泛接受的設計模式的函式庫,而不是教條地使用純粹的 Rust 函式庫。出於這個原因,我們在第三章中選擇使用根據 GTK+ 的函式庫。

如何選擇合適的函式庫

在眾多可用的函式庫中,選擇最合適的函式庫對於專案的成功至關重要。以下是一些選擇函式庫的標準。

功能性與相容性

首先,函式庫必須滿足專案的功能需求。此外,它還需要與專案中使用的其他技術和函式庫相容。

穩定性與維護

函式庫的穩定性和維護狀況也是重要的考慮因素。一個穩定且持續維護的函式庫可以提供更好的支援和更少的錯誤。

熱門程度

如果有多個函式庫滿足上述條件,我們會選擇最熱門的函式庫。熱門程度的評估根據以下因素:

  • 在crates.io上的下載次數
  • 開發和發布的頻率
  • 在問題追蹤器和討論論壇上的討論熱度
  • 媒體報導

雖然熱門程度不能保證成功,但一個熱門的專案更有可能擁有龐大的社群支援,並持續發展。這有助於我們找到具有最大潛力的函式庫,並在未來獲得更多的支援和解答。

本文的使用

本文的章節相互獨立,可以按照任意順序閱讀。然而,一些想法和設計模式會在多個章節中被使用到。我們試圖在設計起源或最有意義的章節中介紹這些想法。例如,在第三章中介紹了使用事件處理程式構建回應式使用者介面的概念,並在第七章中被參照。因此,從頭到尾閱讀本文可能會幫助您逐步建立知識。

章節概述

  • 在第二章中,我們從Rust最簡單的應用程式開始:命令列介面(CLI)。我們介紹如何使用標準函式庫讀取原始引數,然後展示如何使用Clap更好地管理引數。
  • 在第三章中,我們構建了二維介面。首先使用Cursive文字使用者介面系統構建文字介面,然後使用gtk-rs引入Rust對GTK+ 3函式庫的繫結,並構建相同的互動式表單。
  • 在第四章中,我們將Rust編譯為WebAssembly,並使用yew-rs,一個利用WebAssembly建立回應式單頁應用程式的Rust前端網頁框架。
  • 在第五章中,我們探索Rust在傳統的伺服器端渲染網頁和REST API中的應用。我們重建了上一章中的網頁,但這次是作為由Rust actix-web框架提供的伺服器端頁面。
  • 在第六章中,我們探索了將REST API佈署到AWS Lambda Functions和AWS SDK for Rust的最新技術。我們使用AWS DynamoDB表格儲存使用者提供的資訊,並使用S3 Bucket儲存較大的檔案。
  • 在第七章中,我們使用Bevy遊戲引擎製作了一個貓排球遊戲。您將學習Bevy背後的設計哲學和架構,稱為實體元件系統。
  • 在第八章中,我們透過在Raspberry Pi開發板上引入物理計算,將虛擬世界與物理世界連線起來。我們展示如何安裝完整的作業系統和整個Rust工具鏈在裝置上,並介紹如何使用Rust控制LED和取得實體按鈕的輸入。
  • 在第九章中,我們將焦點轉移到人工智慧和機器學習。我們展示如何使用linfa和rusty-machine crate實作無監督和監督機器學習模型。
  • 最後,在第十章中,我們對Rust的其他令人興奮的領域進行了廣泛的概述,包括作業系統、程式語言和分散式系統。

原始碼

本文的所有原始碼都可以在GitHub上找到。當我們在書中包含原始碼時,只會突出顯示與討論相關的部分。非相關部分將被省略,並附有註解。要檢視完整的範例,請檢視GitHub上的原始碼。大多數範例都是在Linux(Ubuntu 22.04)機器上開發和測試的。Rust版本是stable-x86_64-unknown-linux-gnu - rustc 1.68.2。盡可能使用穩定版本,但某些函式庫可能需要使用nightly版本。#### 原始碼解讀: 此段落說明瞭本文原始碼的取得方式與環境組態。

  • 程式碼範例可能不完整,需參照GitHub上的完整版本。
  • 大部分範例根據Linux(Ubuntu 22.04)環境,使用Rust穩定版本(stable-x86_64-unknown-linux-gnu - rustc 1.68.2)。#### 章節內容安排解說: 本文各章節獨立成篇,但仍有內在邏輯聯絡。
  • 第二章介紹CLI開發,涵蓋引數處理、測試及發布流程。
  • 第三章深入TUI與GUI開發,先後使用Cursive與gtk-rs實作互動介面。
  • 後續章節逐步推進至WebAssembly、伺服器端渲染、AWS雲端佈署及遊戲開發等主題。
  • 第九章聚焦於機器學習,介紹無監督與監督式學習模型實作。
  • 第十章展望Rust在作業系統、語言開發等領域的廣泛應用潛力。#### 技術深度與趨勢分析: 本文不僅介紹Rust基本語法,更著重於實務開發技能培養。
  • 從CLI到網頁前端、後端乃至遊戲開發,展現Rust語言的多元應用能力。
  • 涵蓋雲端佈署(AWS Lambda)、嵌入式開發(Raspberry Pi)等前沿主題。
  • 結合機器學習框架(linfa、rusty-machine),拓展讀者的技術視野。 此書完整體現了Rust在不同領域中的實踐應用,有助於讀者全面掌握相關技術與趨勢。

建立命令列程式

命令列程式(Command-line interfaces, CLIs)是 Rust 最自然的應用之一。CLI 是一種從命令列操作的軟體。你透過文字輸入和輸出與它互動。當你編譯第一個“Hello World”程式時,你已經在建立一個命令列程式。典型的命令列程式通常接受引數、旗標,有時還接受標準輸入作為輸入。然後執行其主要演算法並輸出到標準輸出或檔案。所有這些操作都得到了 Rust 標準函式庫和 crates.io(Rust 社群的套件登入檔)上第三方 crate 的良好支援。

為什麼選擇 Rust 來建立 CLI

使用 Rust 建立 CLI 有幾個優勢。首先,crates.io 上豐富的函式庫集合使你能夠在不重新發明輪子的情況下實作許多功能。其次,與其他流行的指令碼語言相比,Rust 具有出色的效能和安全性保證,可以減少許多效能瓶頸和錯誤。最後,Rust 程式可以編譯成單一、小的二進位檔案,包含平台特定的機器碼,便於分發,因此使用者不需要在系統上安裝語言執行環境。

Catsay 專案範例

Cowsay 是一個有趣的小命令列程式,最初用 Perl 編寫。它接受一段文字訊息,並以 ASCII 藝術牛(在我看來更像馬)的形式呈現,並在對話氣泡中說出該訊息(圖 2-1)。雖然這個程式看起來很無用,但它在 Unix 伺服器管理員中仍然相當流行,他們用它向使用者列印輕鬆的歡迎訊息。

Catsay 功能特點

  • 將訊息字串作為位置引數。
  • 接受 -h/--help 旗標以列印幫助訊息。
  • 接受 -d/--dead 旗標,使貓的眼睛變成 xx,這是死亡眼睛的喜劇表現。
  • 以彩色列印。
  • 將錯誤訊息列印到 STDERR 以進行錯誤處理。
  • 接受 STDIN 以進行管道輸入,並將輸出管道傳輸到其他程式。
  • 執行整合測試。
  • 封裝並發布到 crates.io。

建立二進位專案

雖然你可以簡單地編寫一個 .rs 檔案並使用 rustc 編譯它,但這樣處理依賴關係將是一場惡夢。因此,我們將使用 Cargo(Rust 的套件管理器)來管理 Rust 專案並處理依賴關係。

建立 Catsay 專案

要建立一個名為 catsay 的二進位程式,請在終端機中執行以下命令:

$ cargo new --bin catsay

--bin 旗標代表“二進位”,它告訴 Cargo 將套件建立為二進位可執行檔。你可以省略該旗標,因為它是預設值。

檢視 Catsay 專案結構

執行命令後,你應該會看到以下輸出:

Created binary (application) 'catsay' package

該命令建立了一個 catsay 資料夾和一些基本檔案,包括一個 git 倉函式庫,如下所示:

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

開啟 main.rs,你應該會看到由 cargo 建立的 Hello World 程式範本。要執行 Hello World 範例,請先透過執行以下命令進入 catsay 資料夾:

$ cd catsay

內容解密:

  1. cargo new --bin catsay:使用 Cargo 建立一個新的二進位專案,名為 catsay。這是建立 Rust CLI 專案的第一步。
  2. --bin 旗標:指定建立的專案型別為二進位可執行檔。如果省略,Cargo 預設也會建立二進位專案。
  3. Cargo.toml:這是 Cargo 的設定檔,用於管理專案的依賴、版本等資訊。
  4. src/main.rs:這是 Rust 程式的入口點,包含 main 函式,是程式執行的起點。

使用Cargo建立與執行命令列程式

本章節將介紹如何使用Cargo建立一個簡單的命令列程式,並進一步擴充套件其功能以處理命令列引數。

建立與執行Cargo專案

首先,透過Cargo建立一個新的Rust專案:

$ cd catsay
$ cargo run

執行上述指令後,你應該會看到類別似以下的輸出:

Compiling catsay v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 1.77s
Running 'target/debug/catsay'
Hello, world!

使用std::env::args讀取命令列引數

為了使程式能夠接受命令列引數,我們需要使用std::env::args()函式。以下是一個簡單的範例,展示如何修改src/main.rs以讀取第一個命令列引數並列印出來,同時輸出一個ASCII藝術貓:

// src/main.rs
fn main() {
    let message = std::env::args().nth(1)
        .expect("缺少訊息。使用方法: catsay <訊息>");
    println!("{}", message);
    println!(" \\");
    println!(" \\");
    println!(" /\\_/\\");
    println!(" ( o o )");
    println!(" =( I )=");
}

內容解密:

  1. std::env::args()傳回一個迭代器,包含所有命令列引數。
  2. .nth(1)用於取得第一個命令列引數(索引0是程式名稱本身)。
  3. .expect()用於處理可能出現的錯誤,例如當沒有提供任何引數時。
  4. 將取得的引數指定給message變數,並使用println!()宏印出訊息和ASCII藝術貓。

使用Clap處理複雜命令列引數

當命令列程式需要處理多個選項或引數時,手動解析變得複雜。Clap是一個強大的crate,可以簡化這個過程。

首先,在Cargo.toml中加入Clap作為依賴:

# Cargo.toml
[package]
name = "catsay"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = { version = "4.2.1", features = ["derive"] }

然後,執行以下指令以新增Clap:

cargo add clap --features derive

接下來,修改src/main.rs以使用Clap:

// src/main.rs
use clap::Parser;

#[derive(Parser)]
struct Options {
    message: String,
}

fn main() {
    let options = Options::parse();
    let message = options.message;
    println!("{}", message);
    // ... 列印貓的ASCII藝術
}

內容解密:

  1. 定義一個名為Options的結構體,用於儲存命令列引數。
  2. 使用#[derive(Parser)]屬性自動產生解析器程式碼。
  3. main()函式中呼叫Options::parse()來解析命令列引數。
  4. 存取解析後的引數,例如options.message

Clap自動為你的程式產生--help選項,讓你可以檢視程式的使用方法:

$ cargo run -- --help

輸出結果將顯示程式的使用方法和可用的引數。

使用 Clap 增強命令列程式的功能

在開發命令列程式時,提供良好的使用者經驗至關重要。Clap 是一個強大的 Rust 函式庫,能夠幫助開發者輕鬆建立功能豐富的命令列介面。本篇文章將介紹如何使用 Clap 來增強命令列程式的功能。

新增預設值和描述

首先,我們來看看如何為命令列引數新增預設值和描述。在 src/main.rs 中,我們定義了一個 Options 結構體,用於解析命令列引數。

// src/main.rs
struct Options {
    #[clap(default_value = "Meow!")]
    /// What does the cat say?
    message: String,
}

在這個例子中,我們為 message 欄位新增了一個預設值 "Meow!",並提供了描述 "What does the cat say?"。這樣,當使用者執行 --help 命令時,就可以看到相關的描述和預設值。

內容解密:

  • #[clap(default_value = "Meow!")]:為 message 欄位設定預設值。
  • /// What does the cat say?:提供欄位的描述。
  • 這樣設計的好處是,使用者可以清楚地瞭解引數的用途和預設值。

新增布林旗標

接下來,我們來看看如何新增布林旗標。在 src/main.rs 中,我們新增了一個 dead 欄位,用於控制貓的眼睛是否顯示為死亡狀態。

// src/main.rs
struct Options {
    // ...
    #[clap(short = 'd', long = "dead")]
    /// Make the cat appear dead
    dead: bool,
}

在這個例子中,我們定義了一個布林旗標 --dead(或 -d),用於控制貓的眼睛是否顯示為死亡狀態。

內容解密:

  • #[clap(short = 'd', long = "dead")]:定義了短版和長版的旗標。
  • dead: bool:布林欄位,用於儲存旗標的狀態。
  • 當旗標存在時,dead 的值為 true,否則為 false

根據旗標改變程式行為

現在,我們來看看如何根據旗標改變程式的行為。在 main() 函式中,我們根據 dead 旗標的值來決定貓的眼睛是否顯示為死亡狀態。

// src/main.rs
fn main() {
    let options = Options::parse();
    let message = options.message;
    let eye = if options.dead { "x" } else { "o" };
    println!("{}", message);
    println!(" \\");
    println!(" \\");
    println!(" /\\_/\\");
    println!(" ( {eye} {eye} )", eye = eye.red().bold());
    println!(" =( I )=");
}

內容解密:

  • let eye = if options.dead { "x" } else { "o" };:根據 dead 旗標的值決定貓的眼睛是否顯示為死亡狀態。
  • println!(" ( {eye} {eye} )", eye = eye.red().bold());:使用 red()bold() 方法來設定眼睛的顏色和樣式。

輸出到 STDERR

在 Unix-like 系統中,除了 STDOUT 外,還有 STDERR 用於輸出錯誤訊息。Rust 提供了 eprintln!() 巨集,用於輸出到 STDERR。

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

內容解密:

  • eprintln!():用於輸出到 STDERR。
  • 當訊息為 "woof" 時,輸出錯誤訊息到 STDERR。

使用 colored crate 新增顏色

為了使輸出更加豐富多彩,我們可以使用 colored crate。首先,執行 cargo add colored 將 crate 新增到 Cargo.toml 中。

// Cargo.toml
[dependencies]
colored = "2.0.0"

然後,在 src/main.rs 中使用 colored crate 提供的方法來設定輸出的顏色和樣式。

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

內容解密:

  • message.bright_yellow().underline().on_purple():設定訊息的顏色、樣式和背景色。
  • eye.red().bold():設定眼睛的顏色和樣式。
  • 使用 colored crate 可以使輸出更加生動和吸引人。