使用环境变量

我们将通过添加一个额外的功能来改进 `minigrep`:不区分大小写搜索的选项,用户可以通过环境变量来启用它。我们可以将此功能作为命令行选项,并要求用户每次想要应用它时都输入,但通过将其设为环境变量,我们允许用户设置一次环境变量,并在该终端会话中使其所有搜索都不区分大小写。

为不区分大小写的 `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("not enough arguments");
        }

        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`。我们添加了一个新行,文本为 `\"Duct tape.\"`,使用大写字母 *D*,当我们在区分大小写的方式下搜索时,它不应该匹配查询 `\"duct\"`。以这种方式更改旧测试有助于确保我们不会意外破坏我们已经实现的区分大小写搜索功能。此测试现在应该通过,并且在我们处理不区分大小写搜索时应继续通过。

新的不*区分大小写*搜索测试使用 `\"rUsT\"` 作为其查询。在我们即将添加的 `search_case_insensitive` 函数中,查询 `\"rUsT\"` 应该匹配包含 `\"Rust:\"`(带有大写字母 *R*)的行,并匹配行 `\"Trust me.\"`,即使两者的大小写与查询不同。这是我们的失败测试,它将编译失败,因为我们尚未定义 `search_case_insensitive` 函数。您可以随意添加一个始终返回空 vector 的骨架实现,类似于我们在清单 12-16 中对 `search` 函数所做的那样,以查看测试编译并失败。

实现 `search_case_insensitive` 函数

`search_case_insensitive` 函数(如清单 12-21 所示)几乎与 `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("not enough arguments");
        }

        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\"`:该字符串切片不包含我们可以使用的小写 `u` 或 `t`,因此我们必须分配一个新的 `String`,其中包含 `\"rust\"`。当我们现在将 `query` 作为参数传递给 `contains` 方法时,我们需要添加一个与号,因为 `contains` 的签名被定义为接受字符串切片。

接下来,我们在每个 `line` 上添加对 `to_lowercase` 的调用,以将所有字符转换为小写。现在我们已经将 `line` 和 `query` 转换为小写,无论查询的大小写如何,我们都会找到匹配项。

让我们看看这个实现是否通过测试

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [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("not enough arguments");
        }

        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("not enough arguments");
        }

        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` 中的值调用 `search` 或 `search_case_insensitive`

最后,我们需要检查环境变量。用于处理环境变量的函数位于标准库的 `env` 模块中,因此我们将该模块引入 *src/lib.rs* 顶部的作用域。然后,我们将使用 `env` 模块中的 `var` 函数来检查是否已为名为 `IGNORE_CASE` 的环境变量设置了任何值,如清单 12-23 所示。

文件名:src/lib.rs
use std::env;
// --snip--

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("not enough arguments");
        }

        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` 将返回 `false`,并且程序将执行区分大小写的搜索。我们不关心环境变量的*值*,只关心它是否已设置,因此我们正在检查 `is_ok`,而不是使用 `unwrap`、`expect` 或我们在 `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` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!

看起来它仍然有效!现在,让我们在将 `IGNORE_CASE` 设置为 `1` 的情况下运行程序,但使用相同的查询 *to*

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

如果您使用的是 PowerShell,您将需要设置环境变量并分别运行程序命令

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

这将使 `IGNORE_CASE` 在您的 shell 会话的剩余时间内保持持久。可以使用 `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` 模块包含更多用于处理环境变量的有用功能:查看其文档以了解可用的功能。