處理環境變數

我們會新增一個額外的功能來改善 minigrep:使用者可以透過環境變數來啟用不區分大小寫的搜尋功能。我們可以將此功能設為命令列選項並要求使用者每次需要時就要加上它,但是這次我們選擇使用環境變數。這樣一來能讓使用者設置環境變數一次就好,然後在該終端機 session 中所有的搜尋都會是不區分大小寫的。

寫個不區分大小寫的 search 函式的失敗測試

我們首先新增個 search_case_insensitive 函式在環境變數有數值時呼叫它。我們將繼續遵守 TDD 流程,所以第一步一樣是先寫個會失敗的測試。我們會為新函式 search_case_insensitive 新增一個測試,並將舊測試從 one_result 改名為 case_sensitive 以便清楚兩個測試的差別,如範例 12-20 所示。

檔案名稱:src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("引數不足");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

範例 12-20:為準備加入的不區分大小寫函式新增個失敗測試

注意到我們也修改了舊測試 contents。我們新增了一行使用大寫 D 的 "Duct tape.",當我們以區分大小寫來搜尋時,就不會符合要搜尋的 "duct"。這樣變更舊測試能確保我們沒有意外破壞我們已經實作好的區分大小寫的功能。此測試應該要能通過,並在我們實作不區分大小寫的搜尋時仍能繼續通過。

新的不區分大小寫的搜尋測試使用 "rUsT" 來搜尋。在我們準備要加入的 search_case_insensitive 函式中,要搜尋的 "rUsT" 應該要能符合有大寫 R 的 "Rust:" 以及 "Trust me." 這幾行,就算兩者都與搜尋字串有不同的大小寫。這是我們的失敗測試而且它還無法編譯,因為我們還沒有定義 search_case_insensitive 函式。歡迎自行加上一個永遠回傳空向量的骨架實作,就像我們在範例 12-16 所做的一樣,然後看看測試能不能編譯過並失敗。

實作 search_case_insensitive 函式

範例 12-21 顯示的 search_case_insensitive 函式會與 search 函式幾乎一樣。唯一的不同在於我們將 query 與每個 line 都變成小寫,所以無論輸入引數是大寫還是小寫,當我們在檢查行數是否包含搜尋的字串時,它們都會是小寫。

檔案名稱:src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("引數不足");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    for line in search(&config.query, &contents) {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

範例 12-21:定義 search_case_insensitive 並在比較前將搜尋字串與行數均改為小寫

首先我們將 query 字串變成小寫並儲存到同名的遮蔽變數中。我們必須呼叫對要搜尋的字串 to_lowercase,這樣無論使用者輸入的是 "rust""RUST""Rust""rUsT",我們都會將字串視為 "rust" 並以此來不區分大小寫。雖然 to_lowercase 能處理基本的 Unicode,但它不會是 100% 準確的。如果我們是在寫真正的應用程式的話,我們需要處理更多條件,但在此段落是為了理解環境變數而非 Unicode,所以我們維持這樣寫就好。

注意到 query 現在是個 String 而非字串切片,因為呼叫 to_lowercase 會建立新的資料而非參考現存的資料。假設要搜尋的字串是 "rUsT" 的話,該字串切片並沒有包含小寫的 ut 能讓我們來使用,所以我們必須配置一個包含 "rust" 的新 String。現在當我們將 query 作為引數傳給 contains 方法時,我們需要加上「&」,因為 contains 所定義的簽名接收的是一個字串切片。

接著,我們對每個 line 加上 to_lowercase 的呼叫。現在我們將 linequery 都轉換成小寫了。我們可以不區分大小寫來找到符合的行數。

讓我們來看看實作是否能通過測試:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished test [unoptimized + debuginfo] target(s) in 1.33s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests minigrep

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

很好!測試通過。現在讓我們從 run 函式呼叫新的 search_case_insensitive 函式。首先,我們要在 Config 中新增一個配置選項來切換區分大小寫與不區分大小寫之間的搜尋。新增此欄位會造成編譯錯誤,因為我們還沒有初始化該欄位:

檔案名稱:src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("引數不足");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

注意到我們新增了 ignore_case 欄位並存有布林值。接著,我們需要 run 函式檢查 ignore_case 欄位的數值並以此決定要呼叫 search 函式或是 search_case_insensitive 函式,如範例 12-22 所示。不過目前還無法編譯。

檔案名稱:src/lib.rs

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub ignore_case: bool,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("引數不足");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        Ok(Config { query, file_path })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

範例 12-22:依據 config.ignore_case 的數值來呼叫 searchsearch_case_insensitive

最後,我們需要檢查環境變數。處理環境變數的函式位於標準函式庫中的 env 模組中,所以我們在 src/lib.rs 最上方加上 use std::env; 來將該模組引入作用域。然後我們使用 env 模組中的 var 函式來檢查一個叫做 IGNORE_CASE 的環境變數有沒有設任何數值,如範例 12-23 所示。

檔案名稱:src/lib.rs

use std::env;
// --省略--

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub file_path: String,
    pub case_sensitive: bool,
}

impl Config {
    pub fn build(args: &[String]) -> Result<Config, &'static str> {
        if args.len() < 3 {
            return Err("引數不足");
        }

        let query = args[1].clone();
        let file_path = args[2].clone();

        let ignore_case = env::var("IGNORE_CASE").is_ok();

        Ok(Config {
            query,
            file_path,
            ignore_case,
        })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.file_path)?;

    let results = if config.ignore_case {
        search_case_insensitive(&config.query, &contents)
    } else {
        search(&config.query, &contents)
    };

    for line in results {
        println!("{line}");
    }

    Ok(())
}

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

pub fn search_case_insensitive<'a>(
    query: &str,
    contents: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }

    results
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";

        assert_eq!(
            vec!["Rust:", "Trust me."],
            search_case_insensitive(query, contents)
        );
    }
}

範例 12-23:檢查環境變數 IGNORE_CASE

我們在此建立了一個新的變數 ignore_case。要設置其數值,我們可以呼叫 env::var 函式並傳入環境變數 IGNORE_CASE 的名稱。env::var 函式會回傳 Result,如果有設置環境變數的話,這就會是包含環境變數數值的成功 Ok變體;如果環境變數沒有設置的話,這就會回傳 Err 變體。

我們在 Result 使用 is_ok 方法來檢查環境變數是否有設置,也就是程式是否該使用區分大小寫的搜尋。如果 IGNORE_CASE 環境變數沒有設置任何數值的話,is_ok 會回傳否,所以程式就會進行區分大小寫的搜尋。我們不在乎環境變數的數值,只在意它有沒有被設置而已,所以我們使用 is_ok 來檢查而非使用 unwrapexpect 或其他任何我們看過的 Result 方法。

我們將變數 ignore_case 的數值傳給 Config 實例,讓 run 函式可以讀取該數值並決定該呼叫 search_case_insensitive 還是 search,如範例 12-22 所實作的一樣。

讓我們試看看吧!首先,我們先不設置環境變數並執行程式來搜尋 to,任何包含小寫單字「to」的行數都應要符合:

$ cargo run -- to poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

看起來運作仍十分正常!現在,讓我們設置 IGNORE_CASE1 並執行程式來搜尋相同的字串 to

$ IGNORE_CASE=1 cargo run -- to poem.txt

如果你使用的是 PowerShell,你需要將設置變數與執行程式分為不同的命令:

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt

這會在你的 shell session 中設置 IGNORE_CASE。它可以透過 Remove-Item cmdlet 來取消設置:

PS> Remove-Item Env:IGNORE_CASE

我們應該會得到包含可能有大寫的「to」的行數:

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

太好了,我們也取得了包含「To」的行數!我們的 minigrep 程式現在可以進行不區分大小寫的搜尋並以環境變數配置。現在你知道如何使用命令列引數或環境變數來管理設置選項了。

有些程式允許同時使用引數環境變數來配置。在這種情況下,程式會決定各種選項的優先層級。你想要練習的話,嘗試使用命令列引數與環境變數來控制區分大小寫的選項。並在程式執行時,其中一個設置為區分大小寫,而另一個為不區分大小寫時,自行決定該優先使用命令列引數還是環境變數。

std::env 模組還包含很多處理環境變數的實用功能,歡迎查閱其官方文件來瞭解有哪些可用。