剖析命令列引數

我們 CLI 工具的典型呼叫方式如下:

$ grrs foobar test.txt

我們預期我們的程式會查看 test.txt 並列印出包含 foobar 的行。但是我們要如何取得這兩個值?

程式名稱後的文字通常稱為「命令列引數」或「命令列旗標」(特別是當它們看起來像 --this)。在內部,作業系統通常會將它們表示成一個字串清單 - 粗略來說,它們會以空格分隔。

有很多方式可以思考這些引數,以及如何將它們剖析成更容易操作的內容。您還需要告訴您的程式使用者他們需要提供哪些引數以及預期格式。

取得引數

標準函式庫包含函式 std::env::args(),它提供了這些引數的 反覆子。第一個條目(索引值 0)將是您的程式被呼叫的名稱(例如 grrs),後面的那些是使用者之後寫的內容。

使用這種方法取得原始引數相當容易(在檔案 src/main.rs 中)

fn main() {
    let pattern = std::env::args().nth(1).expect("no pattern given");
    let path = std::env::args().nth(2).expect("no path given");

    println!("pattern: {:?}, path: {:?}", pattern, path)
}

我們可以使用 cargo run 執行它,在 -- 之後傳遞引數再寫入引數

$ cargo run -- some-pattern some-file
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `target/debug/grrs some-pattern some-file`
pattern: "some-pattern", path: "some-file"

CLI 引數當作資料型態

與其將它們視為一堆文字,通常可以將 CLI 引數視為代表您的程式輸入的客製資料型別。

檢視 grrs foobar test.txt:有兩個引數,第一個是 pattern(要尋找的字串),然後是 path(要檢視的檔案)。

我們還能說些什麼呢?嗯,首先,這兩者都是必需的。我們還沒有談論任何預設值,因此我們期望使用者始終提供兩個值。此外,我們可以談談它們的類型:程式碼預期是一個字串,而第二個參數預期是一個檔案路徑。

在 Rust 中,根據處理的資料來建構程式很常見,因此這種檢視 CLI 參數的方式非常合適。讓我們從這裡開始(在檔案 src/main.rs 中,在 fn main() { 之前)

struct Cli {
    pattern: String,
    path: std::path::PathBuf,
}

這定義了一個新的結構(一個 struct),它有兩個欄位來儲存資料:patternpath

現在,我們仍然需要將程式實際獲得的參數轉換成這種形式。一個方法是手動剖析我們從作業系統得到的字串清單並自己建構結構。它看起來像這樣

fn main() {
    let pattern = std::env::args().nth(1).expect("no pattern given");
    let path = std::env::args().nth(2).expect("no path given");

    let args = Cli {
        pattern: pattern,
        path: std::path::PathBuf::from(path),
    };

    println!("pattern: {:?}, path: {:?}", args.pattern, args.path);
}

這樣做可以,但不太方便。你將如何處理支援 --pattern="foo"--pattern "foo" 的需求?你將如何實作 --help

使用 Clap 剖析 CLI 參數

一個更好的方法是使用眾多可用函式庫之一。用於剖析命令列參數最受歡迎的函式庫稱為 clap。它具備您預期的所有功能,包括對子命令、殼層完成 和出色的說明訊息的支援。

讓我們先匯入 clap,方法是將 clap = { version = "4.0", features = ["derive"] } 新增到 Cargo.toml 檔案的 [dependencies] 區段。

現在,我們可以在程式碼中撰寫 use clap::Parser;,並在我們的 struct Cli 正上方新增 #[derive(Parser)]。讓我們也寫一些文件註解。

它看起來像這樣(在檔案 src/main.rs 中,在 fn main() { 之前)

use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

我們的範本就在 Cli 結構的正下方,包含其 main 函式。當程式啟動時,它將呼叫此函式

fn main() {
    let args = Cli::parse();

    println!("pattern: {:?}, path: {:?}", args.pattern, args.path)
}

這將嘗試將參數剖析到我們的 Cli 結構中。

但是,如果它失敗了怎麼辦?這就是此方法的好處:Clap 知道要預期的欄位以及它們預期的格式。它可以自動產生一個漂亮的 --help 訊息,並提供一些很棒的錯誤,建議您在撰寫 --putput 時傳遞 --output

整理

你的程式碼現在應該如下所示

use clap::Parser;

/// Search for a pattern in a file and display the lines that contain it.
#[derive(Parser)]
struct Cli {
    /// The pattern to look for
    pattern: String,
    /// The path to the file to read
    path: std::path::PathBuf,
}

fn main() {
    let args = Cli::parse();

    println!("pattern: {:?}, path: {:?}", args.pattern, args.path)
}

執行時不輸入任何引數

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 10.16s
     Running `target/debug/grrs`
error: The following required arguments were not provided:
    <pattern>
    <path>

USAGE:
    grrs <pattern> <path>

For more information try --help

執行時輸入引數

$ cargo run -- some-pattern some-file
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `target/debug/grrs some-pattern some-file`
pattern: "some-pattern", path: "some-file"

輸出結果顯示我們的程式已成功將引數剖析到 Cli 結構。