接受命令列引數

一如往常我們用 cargo new 建立新的專案,我們將我們的專案命名為 minigrep 來與很可能已經在你系統中的 grep 工具做區別。

$ cargo new minigrep
     Created binary (application) `minigrep` project
$ cd minigrep

第一項任務是要讓 minigrep 能接收兩個命令列引數:檔案路徑與欲搜尋的字串。也就是說,我們想要能夠使用 cargo run 加上兩條連字號來指示接下來的引號用於我們的程式而不是 cargo,然後輸入欲搜尋的字串與要被搜尋的檔案路徑來執行程式,如以下所示:

$ cargo run -- searchstring example-filename.txt

但現在由 cargo new 產生的程式還無法處理我們給予的引數。crates.io 有些函式庫可以幫助程式接收命令列中的引數,但有鑑於你要學習此概念,讓我們親自來實作一個。

讀取引數數值

要讓 minigrep 能夠讀取我們傳入的命令列引數數值,我們需要使用 Rust 標準函式庫中提供的 std::env::args 函式。此函式會回傳一個包含我們傳給 minigrep 的命令列引數的疊代器(iterator)。我們會在第十三章詳細解釋疊代器。現在你只需要知道疊代器的兩項重點:疊代器會產生一系列的數值,然後我們可以對疊代器呼叫 collect 方法來將其轉換成像是向量的集合,來包含疊代器產生的所有元素。

範例 21-1 的程式碼能讓你的 minigrep 程式能夠讀取任何傳入的命令列引數,然後收集數值成一個向量。

檔案名稱:src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    dbg!(args);
}

範例 12-1:收集命令列引數至向量中並顯示它們

首先我們透過 use 陳述式將 std::env 模組引入作用域,讓我們可以使用它的 args 函式。注意到 std::env::args 函式位於兩層模組下。如同我們在第七章談過的,如果我們要用的函式模組路徑超過一層以上的話,我們選擇將上層模組引入作用域中,而不是函式本身。這樣的話,我們可以輕鬆使用 std::env 中的其他函式。而且這也比直接加上 use std::env::args 然後只使用 args 來呼叫函式還要明確些,因為 args 容易被誤認成是由目前模組定義的函式。

args 函式與無效的 Unicode

值得注意的是如果任何引數包含無效 Unicode 的話,std::env::args 就會恐慌。如果你的程式想要接受包含無效 Unicode 引數的話,請改使用 std::env::args_os。該函式回傳會產生 OsString 數值的疊代器,而非 String 數值。我們出於簡單方便所以在此使用 std::env::args,因為 OsString 在不同平台中數值會有所差異,且會比 String 數值還要難處理。

我們在 main 中的第一行呼叫 env::args,然後馬上使用 collect 來將疊代器轉換成向量,這會包含疊代器產生的所有數值。我們可以使用 collect 函式來建立許多種集合,所以我們顯式詮釋 args 的型別來指定我們想要字串向量。雖然我們很少需要在 Rust 中詮釋型別,collect 是其中一個你常常需要詮釋的函式,因為 Rust 無法推斷出你想要何種集合。

最後,我們使用除錯巨集來顯示向量。讓我們先嘗試不用引數來執行程式碼,再用兩個引數來執行:

$ cargo run
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/minigrep`
[src/main.rs:5] args = [
    "target/debug/minigrep",
]
$ cargo run -- needle haystack
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 1.57s
     Running `target/debug/minigrep needle haystack`
[src/main.rs:5] args = [
    "target/debug/minigrep",
    "needle",
    "haystack",
]

值得注意的是向量中第一個數值為 "target/debug/minigrep",這是我們的執行檔名稱。這與 C 的引數列表行為相符,讓程式在執行時能使用它們被呼叫的名稱路徑。存取程式名稱通常是很實用的,像是你能將它顯示在訊息中,或是依據程式被呼叫的命令列別名來改變程式的行為。但考慮本章節的目的,我們會忽略它並只儲存我們想要的兩個引數。

將引數數值儲存至變數

目前程式能夠取得命令列引數指定的數值。現在我們想要將這兩個引數存入變數中,讓我們可以在接下來的程式中使用數值,如範例 12-2 所示。

檔案名稱:src/main.rs

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("搜尋 {}", query);
    println!("目標檔案為 {}", file_path);
}

範例 12-2:建立變數來儲存搜尋引數與檔案路徑引數

如我們印出向量時所看到的,向量的第一個數值 args[0] 會是程式名稱,所以我們從引數 1 開始。minigrep 接收的第一個引數會是我們要搜尋的字串,所以我們將第一個引數的參考賦值給變數 query。第二個引數會是檔案路徑,所以我們將第二個引數的參考賦值給 file_path

我們暫時印出這些變數的數值來證明程式碼運作無誤。讓我們用引數 testsample.txt 來再次執行程式:

$ cargo run -- test sample.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep test sample.txt`
搜尋 test
目標檔案為 sample.txt

很好,程式能執行!我們想要的引數數值都有儲存至正確的變數中。之後我們會對特定潛在錯誤的情形來加上一些錯誤處理,像是當使用者沒有提供引數的情況。現在我們先忽略這樣的情況,並開始加上讀取檔案的功能。