使用环境变量

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

为不区分大小写的测试编写失败的测试search功能

我们首先添加一个新的search_case_insensitive函数,该函数将在 环境变量具有值。我们将继续遵循 TDD 流程, 所以第一步是再次编写一个失败的测试。我们将为 新的search_case_insensitive函数,并将旧测试从one_resultcase_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,类似于我们所做的 对于search示例 12-16 中的函数来查看测试 compile 和 fail。

实施search_case_insensitive功能

search_case_insensitive函数,如示例 12-21 所示,将几乎是 与search功能。唯一的区别是我们将小写 这query和每个line因此,无论 input 参数的情况如何, 当我们检查该行是否包含查询时,它们的情况相同。

文件名: 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函数将查询和行小写,然后再比较它们

首先,我们将querystring 并将其存储在一个带有 相同的名称。叫to_lowercase上查询是必需的,因此没有 无论用户的查询是否为"rust","RUST","Rust""rUsT", 我们将查询视为"rust"并且对案件不敏感。 而to_lowercase将处理基本的 Unicode,它不会是 100% 准确的。如果 我们正在编写一个真实的应用程序,我们想在这里做更多的工作,但是 本节是关于环境变量的,而不是 Unicode,所以我们把它留在 那个在这里。

请注意,query现在是String而不是字符串 slice,因为调用to_lowercase创建新数据,而不是引用现有数据。说出 query 为"rUsT",例如:该字符串 slice 不包含小写ut供我们使用,因此我们必须分配一个新的String"rust".当我们通过query作为contains方法,我们 需要添加 & 符号,因为contains定义为采用 字符串切片。

接下来,我们添加对to_lowercase在每个line全部小写 字符。现在我们已经转换了linequery改为小写,我们将 无论查询的情况如何,都可以查找匹配项。

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

$ 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

伟大!他们通过了。现在,让我们调用新的search_case_insensitive功能 从run功能。首先,我们将向Configstruct 在区分大小写和不区分大小写的搜索之间切换。添加 此字段将导致编译器错误,因为我们没有初始化此字段 ANYWHERE 尚未:

文件名: 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_casefield 的值,并使用它来决定 是否调用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:调用searchsearch_case_insensitive基于config.ignore_case

最后,我们需要检查环境变量。的函数 使用环境变量位于env标准 库,因此我们将该模块放入 src/lib.rs 顶部的范围中。然后 我们将使用var函数envmodule 来检查是否有任何值 已为名为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那将是 成功的Okvariant 中,如果 环境变量设置为任何值。它将返回Err变体 如果未设置环境变量。

我们使用的是is_ok方法上的Result检查环境 变量,这意味着程序应该执行不区分大小写的搜索。 如果IGNORE_CASE环境变量未设置为任何内容,则is_ok将 返回false程序将执行区分大小写的搜索。我们没有 关心环境变量的值,只是它是 set 还是 unset 的 URL 中,因此我们要检查is_ok而不是使用unwrap,expect或任何 我们在Result.

我们将ignore_case变量添加到Config实例,因此runfunction 可以读取该值并决定是否调用search_case_insensitivesearch,正如我们在示例 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但使用相同的查询

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

如果您使用的是 PowerShell,则需要设置环境变量和 将程序作为单独的命令运行:

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

这将使IGNORE_CASEpersist for your shell session 的剩余时间。 可以使用Remove-Itemcmdlet 中:

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程序现在可以执行 由环境变量控制的不区分大小写的搜索。现在您知道了 如何使用命令行参数或环境管理选项集 变量。

一些程序允许使用相同的参数环境变量 配置。在这些情况下,程序决定一个或另一个需要 优先。对于您自己的另一个练习,请尝试控制区分大小写 通过命令行参数或环境变量。决定 命令行参数还是环境变量应采用 优先级 (如果程序运行时 1 设置为 区分大小写),1 设置为 ignore 大小写。

std::env模块包含更多有用的功能来处理 环境变量:查看其文档以了解可用的内容。

本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准