Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

使用环境变量

我们将为 minigrep 添加一个额外的功能:一个通过环境变量开启的大小写不敏感搜索选项。我们可以将这个功能做成命令行选项,要求用户每次使用时都输入,但将其设为环境变量后,用户只需设置一次环境变量,就可以在该终端会话中进行大小写不敏感的搜索。

为大小写不敏感搜索编写一个失败的测试

我们首先在 minigrep 库中添加一个新的 search_case_insensitive 函数,当环境变量有值时将调用该函数。我们将继续遵循 TDD 流程,所以第一步仍然是编写一个失败的测试。我们将为新的 search_case_insensitive 函数添加一个新测试,并将旧测试从 one_result 重命名为 case_sensitive,以明确两个测试之间的区别,如示例 12-20 所示。

Filename: src/lib.rs
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)
        );
    }
}
Listing 12-20: 为即将添加的大小写不敏感函数添加一个新的失败测试

注意我们也修改了旧测试的 contents。我们添加了一行新文本 "Duct tape.",使用了大写的 D,在大小写敏感搜索时不应匹配查询 "duct"。以这种方式修改旧测试有助于确保我们不会意外破坏已经实现的大小写敏感搜索功能。这个测试现在应该能通过,并且在我们实现大小写不敏感搜索时也应该继续通过。

大小写不敏感搜索的新测试使用 "rUsT" 作为查询字符串。在我们即将添加的 search_case_insensitive 函数中,查询 "rUsT" 应该匹配包含 "Rust:" 的行(大写 R)以及 "Trust me." 这一行,即使它们的大小写与查询不同。这是我们的失败测试,由于我们还没有定义 search_case_insensitive 函数,它将无法编译。你可以像我们在示例 12-16 中为 search 函数所做的那样,添加一个始终返回空 vector 的骨架实现,以查看测试编译并失败的情况。

实现 search_case_insensitive 函数

search_case_insensitive 函数如示例 12-21 所示,与 search 函数几乎相同。唯一的区别是我们会将 query 和每一行 line 都转换为小写,这样无论输入参数的大小写如何,在检查该行是否包含查询字符串时它们都是相同的大小写。

Filename: src/lib.rs
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)
        );
    }
}
Listing 12-21: 定义 search_case_insensitive 函数,在比较之前将查询字符串和行都转换为小写

首先,我们将 query 字符串转换为小写并存储在一个同名的新变量中,遮蔽了原来的 query。对查询字符串调用 to_lowercase 是必要的,这样无论用户的查询是 "rust""RUST""Rust" 还是 "rUsT",我们都会将查询视为 "rust",从而实现大小写不敏感。虽然 to_lowercase 能处理基本的 Unicode,但不会百分之百准确。如果我们在编写一个真正的应用程序,这里需要做更多工作,但本节的重点是环境变量而非 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` 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/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


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

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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 })
    }
}

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(())
}

我们添加了一个保存布尔值的 ignore_case 字段。接下来,我们需要让 run 函数检查 ignore_case 字段的值,并据此决定是调用 search 函数还是 search_case_insensitive 函数,如示例 12-22 所示。这段代码仍然无法编译。

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

// --snip--


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

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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 })
    }
}

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(())
}
Listing 12-22: 根据 config.ignore_case 的值调用 searchsearch_case_insensitive

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

Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{search, search_case_insensitive};

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

    let config = Config::build(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {err}");
        process::exit(1);
    });

    if let Err(e) = run(config) {
        println!("Application error: {e}");
        process::exit(1);
    }
}

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

impl Config {
    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,
        })
    }
}

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(())
}
Listing 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 而非 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` 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 模块还包含许多处理环境变量的实用功能:查看其文档以了解可用的内容。