使用环境变量
我们将通过添加一个额外的功能来改进 minigrep
:一个用户可以通过环境变量打开的不区分大小写的搜索选项。我们可以将此功能设为命令行选项,并要求用户在每次希望应用时输入它,但通过将其设为环境变量,我们允许用户设置一次环境变量,并在该终端会话中让他们的所有搜索不区分大小写。
为不区分大小写的搜索
函数编写失败的测试
我们首先添加新的 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:为我们将要添加的不区分大小写的函数添加新的失败测试
请注意,我们也编辑了旧测试的内容
。我们添加了一行新行,其中包含文本 “Duct tape.”
,使用不应与查询匹配的大写 D
“duct”
来描述。以这种方式更改旧测试有助于确保我们不会意外破坏我们已经实现的区分大小写的搜索功能。此测试现在应该通过,并且在我们进行不区分大小写的搜索时应该继续通过。
不区分大小写的搜索的新测试使用 “rUsT”
作为其查询。在我们即将添加的 search_case_insensitive
函数中,查询 “rUsT”
应匹配包含大写 R 的 “Rust:”
的行,并匹配 “Trust me” 行。
即使两者的大小写与查询不同。这是我们失败的测试,它将无法编译,因为我们还没有定义 search_case_insensitive
函数。随意添加一个始终返回空 vector 的框架实现,类似于示例 12-16 中对 search
函数所做的方式,以查看测试编译和失败。
实现 search_case_insensitive
函数
示例 12-21 所示的 search_case_insensitive
函数与 search
函数几乎相同。唯一的区别是我们将查询
和每一行
小写,这样无论输入参数的大小写如何,当我们检查该行是否包含查询时,它们的大小写都是相同的。
文件名: 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 和 line 之前将它们小写
首先,我们将查询
字符串小写并将其存储在具有相同名称的阴影变量中。对查询调用 to_lowercase
是必要的,这样无论用户的查询是 “rust”、“
RUST”、“
Rust”
还是 “rUsT”
,我们都会将查询视为 “rust”
,并且对大小写不敏感。虽然 to_lowercase
可以处理基本的 Unicode,但它不会 100% 准确。如果我们要编写一个真正的应用程序,我们想在这里做更多的工作,但本节是关于环境变量的,而不是 Unicode,所以我们就在这里就这样说了。
请注意,query
现在是 String
而不是 String 切片,因为调用
to_lowercase
创建新数据,而不是引用现有数据。假设查询是 “rUsT”
,例如:该字符串 slice 不包含小写
u
或 t
供我们使用,因此我们必须分配一个新的 String
,其中包含
“rust”
的 intent 命令。现在,当我们将 query
作为参数传递给 contains
方法时,我们需要添加一个 & 符号,因为 contains
的签名被定义为采用字符串切片。
接下来,我们在每行
添加对 to_lowercase
的调用,以将所有字符小写。现在我们已经将 line
和 query
转换为小写,无论 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
中添加一个配置选项
struct 在区分大小写和不区分大小写的搜索之间切换。添加
此字段将导致编译器错误,因为我们没有初始化此字段
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
字段。接下来,我们需要运行
函数来检查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,该 Result
将是包含环境变量值的成功 Ok
变体。如果未设置环境变量,它将返回 Err
变体。
我们在 Result
上使用 is_ok
方法来检查是否设置了环境变量,这意味着程序应该执行不区分大小写的搜索。如果 IGNORE_CASE
环境变量未设置为任何值,则 is_ok
将返回 false
,并且程序将执行区分大小写的搜索。我们不关心环境变量的值,只关心它是 set 还是 unset,因此我们检查 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
,但使用相同的查询来:
$ 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
模块包含更多用于处理环境变量的有用功能:查看其文档以了解可用的功能。