我们将minigrep通过添加一个额外的功能进行改进:用户可以通过环境变量打开的不区分大小的搜索选项。我们可以将此功能设置为命令行选项,并要求用户在每次希望应用该功能时都输入该功能,但我们将使用环境变量。这样做使我们的用户可以一次设置环境变量,并使所有搜索在该终端会话中不区分大小写。
为不区分大小写的search功能编写失败测试
我们想添加一个新search_case_insensitive函数,当环境变量打开时,我们将调用它。我们将继续遵循TDD流程,因此第一步再次是编写失败的测试。我们将为新search_case_insensitive功能添加一个新测试,并将旧测试从重命名为 one_result,case_sensitive以阐明这两个测试之间的差异,如清单12-20所示。
文件名:src / lib.rs
#[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在文本中添加了新行,该大写字母D应该与查询不匹配 "duct"。以这种方式更改旧测试有助于确保我们不会意外破坏已经实现的区分大小写的搜索功能。该测试现在应该通过,并且在我们进行不区分大小写的搜索时应该继续通过。
不区分大小写搜索的新测试"rUsT"用作其查询。在search_case_insensitive我们将要添加的函数中,查询"rUsT" 应匹配包含"Rust:"大写字母R的行,并且"Trust me."即使行和 查询的大小写都不同,也应匹配该行。这是我们失败的测试,由于我们尚未定义search_case_insensitive函数,因此它将无法编译。随意添加一个始终返回空向量的框架实现,类似于search清单12-16中的函数可以看到测试编译和失败的方式。
实现search_case_insensitive功能
search_case_insensitive清单12-21中所示的函数将与该search函数几乎相同。唯一的区别是,我们将小写query和,line因此无论输入参数的大小写如何,当我们检查行是否包含查询时,它们都是相同的大小写。
文件名:src / lib.rs
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
}
清单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供我们使用,因此我们必须分配一个新的Stringcontains "rust"。现在,当我们query作为参数传递给contains方法时,我们需要添加一个&符号,因为的签名contains已定义为采用字符串切片。
接下来,在检查to_lowercase每个字符line是否包含query小写的所有字符之前,先在上添加一个调用。现在,我们已经转换line ,并query为小写,我们会发现比赛无论查询的情况下是什么。
让我们看看该实现是否通过测试:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished test [unoptimized + debuginfo] target(s) in 1.33s
Running target/debug/deps/minigrep-4672b652f7794785
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
Running target/debug/deps/minigrep-caf9dbee196c78b9
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
大!他们通过了。现在,让我们search_case_insensitive从run函数中调用新函数。首先,我们将在Config结构中添加配置选项,以在 区分大小写的搜索和不区分大小写的搜索之间切换。添加此字段将导致编译器错误,因为我们尚未在任何地方初始化此字段:
文件名:src / lib.rs
pub struct Config {
pub query: String,
pub filename: String,
pub case_sensitive: bool,
}
请注意,我们添加了case_sensitive包含布尔值的字段。接下来,我们需要该run函数检查该case_sensitive字段的值,并使用该值来决定是调用该search函数还是调用该 search_case_insensitive函数,如清单12-22所示。请注意,这仍然不会编译。
文件名:src / lib.rs
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.filename)?;
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
代码清单12-22:根据中的值调用search或 search_case_insensitiveconfig.case_sensitive
最后,我们需要检查环境变量。用于处理环境变量的函数env位于标准库的模块中,因此我们希望将该模块引入作用域,use std::env;并在src / lib.rs顶部加一行。然后,我们将使用 模块中的var函数env来检查名为的环境变量CASE_INSENSITIVE,如清单12-23所示。
文件名:src / lib.rs
use std::env;
// --snip--
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config {
query,
filename,
case_sensitive,
})
}
}
清单12-23:检查名为的环境变量 CASE_INSENSITIVE
在这里,我们创建一个新变量case_sensitive。要设置其值,我们调用该 env::var函数并将其传递给CASE_INSENSITIVE环境变量的名称。如果设置了环境变量,该env::var函数将返回a Result,它将是成功的 Ok变量,其中包含环境变量的值。Err如果未设置环境变量,它将返回变量。
我们正在使用上的is_err方法Result来检查是否为错误并因此未设置,这意味着它应该进行区分大小写的搜索。如果将 CASE_INSENSITIVE环境变量设置为任何值,is_err将返回false,并且程序将执行不区分大小写的搜索。我们不关心值的环境变量,只是无论是设置或取消,所以我们正在检查is_err,而不是使用unwrap,expect或任何我们所见过的其他方法Result。
我们将case_sensitive变量中的值传递给Config实例,以便run函数可以读取该值并决定是否调用search或 search_case_insensitive,如清单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!
看起来仍然有效!现在,让我们使用CASE_INSENSITIVE 设置为,1但使用相同的query来运行程序to。
如果使用PowerShell,则需要设置环境变量并作为单独的命令运行程序:
PS> $Env:CASE_INSENSITIVE=1; cargo run to poem.txt
这将使CASE_INSENSITIVE其余的Shell会话保持不变。可以使用Remove-Itemcmdlet取消设置:
PS> Remove-Item Env:CASE_INSENSITIVE
我们应该获得包含“ to”的行,其中可能包含大写字母:
$ CASE_INSENSITIVE=1 cargo run to poem.txt
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!
To tell your name the livelong day
To an admiring bog!
太好了,我们还包含“ To”的行!我们的minigrep程序现在可以执行由环境变量控制的不区分大小写的搜索。现在您知道了如何使用命令行参数或环境变量来管理选项集。
一些程序允许参数和环境变量用于同一配置。在这些情况下,程序将决定一个或另一个优先。对于您自己的另一项练习,请尝试通过命令行参数或环境变量来控制不区分大小写。确定如果程序以一组区分大小写的方式运行,并且以一组不区分大小写的方式运行,则应优先使用命令行参数还是环境变量。
该std::env模块包含许多用于处理环境变量的有用功能:查阅其文档以了解可用的内容。