处理环境变量
ch12-05-working-with-environment-variables.md
commit 0db6a0a34886bf02feabcab8b430b5d332a8bdf5
我们将用一个额外的功能来改进我们的工具:一个通过环境变量启用的大小写不敏感搜索的选项。我们将其设计为一个命令行参数并要求用户每次需要时都加上它,不过相反我们将使用环境变量。这允许用户设置环境变脸一次之后在整个终端会话中所有的搜索都将是大小写不敏感的了。
编写一个大小写不敏感search
函数的失败测试
首先,增加一个新函数,当设置了环境变量时会调用它。
这里将继续遵循上一部分开始使用的 TDD 过程,其第一步是再次编写一个失败测试。我们将为新的大小写不敏感搜索函数新增一个测试函数,并将老的测试函数从one_result
改名为case_sensitive
来更清除的表明这两个测试的区别,如列表 12-20 所示:
Filename: src/lib.rs
#[cfg(test)]
mod test {
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)
);
}
}
注意我们也改变了老测试中query
和contents
的值:将查询字符串改变为 "duct",它将会匹配带有单词 productive" 的行。还新增了一个含有文本 "Duct tape" 的行,它有一个大写的 D,这在大小写敏感搜索时不应该匹配 "duct"。我们修改这个测试以确保不会意外破坏已经实现的大小写敏感搜索功能;这个测试现在应该能通过并在处理大小写不敏感搜索时应该能一直通过。
大小写不敏感搜索的新测试使用带有一些大写字母的 "rUsT" 作为其查询字符串。我们将要增加的search_case_insensitive
的期望返回值是包含查询字符串 "rust" 的两行,"Rust:" 包含一个大写的 R 还有"Trust me."包含一个小写的 r。这个测试现在会编译失败因为还没有定义search_case_insensitive
函数;请随意增加一个总是返回空 vector 的骨架实现,正如列表 12-16 中search
函数那样为了使测试编译并失败时所做的那样。
实现search_case_insensitive
函数
search_case_insensitive
函数,如列表 12-21 所示,将与search
函数基本相同。区别是它会将query
变量和每一line
都变为小写,这样不管输入参数是大写还是小写,在检查该行是否包含查询字符串时都会是小写。
Filename: src/lib.rs
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
}
首先我们将query
字符串转换为小写,并将其储存(覆盖)到同名的变量中。对查询字符串调用to_lowercase
是必需的这样不管用户的查询是"rust"、"RUST"、"Rust"或者"rUsT",我们都将其当作"rust"处理并对大小写不敏感。
注意query
现在是一个String
而不是字符串 slice,因为调用to_lowercase
是在创建新数据,而不是引用现有数据。如果查询字符串是"rUsT",这个字符串 slice 并不包含可供我们使用的小写的 u,所以必需分配一个包含"rust"的新String
。因为query
现在是一个String
,当我们将query
作为一个参数传递给contains
方法时,需要增加一个 & 因为contains
的签名被定义为获取一个字符串 slice。
接下来在检查每个line
是否包含search
之前增加了一个to_lowercase
调用。这会将"Rust:"变为"rust:"并将"Trust me."变为"trust me."。现在我们将line
和query
都转换成了小写,这样就可以不管大小写的匹配文件中的文本和用户输入的查询了。
让我们看看这个实现能否通过测试:
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/greprs-e58e9b12d35dc861
running 2 tests
test test::case_insensitive ... ok
test test::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured
Running target/debug/greprs-8a7faa2662b5030a
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
Doc-tests greprs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured
好的!现在,让我们在run
函数中调用真正的新search_case_insensitive
函数。首先,我们将在Config
结构体中增加一个配置项来切换大小写敏感和大小写不敏感搜索。
Filename: 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所示:
Filename: src/lib.rs
# use std::error::Error;
# use std::fs::File;
# use std::io::prelude::*;
#
# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
# vec![]
# }
#
# fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
# vec![]
# }
#
# struct Config {
# query: String,
# filename: String,
# case_sensitive: bool,
# }
#
pub fn run(config: Config) -> Result<(), Box<Error>>{
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
let results = if config.case_sensitive {
search(&config.query, &contents)
} else {
search_case_insensitive(&config.query, &contents)
};
for line in results {
println!("{}", line);
}
Ok(())
}
最后需要实际检查环境变量。处理环境变量的函数位于标准库的env
模块中,所以我们需要在 src/lib.rs 的开头增加一个use std::env;
行将这个模块引入作用域中。接着在Config::new
中使用env
模块的var
方法检查一个叫做CASE_INSENSITIVE
的环境变量,如列表 12-23 所示:
Filename: src/lib.rs
use std::env;
# struct Config {
# query: String,
# filename: String,
# case_sensitive: bool,
# }
// ...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: query,
filename: filename,
case_sensitive: case_sensitive,
})
}
}
这里创建了一个新变量case_sensitive
。为了设置它的值,需要调用env::var
函数并传递我们需要寻找的环境变量名称,CASE_INSENSITIVE
。env::var
返回一个Result
,它在环境变量被设置时返回包含其值的Ok
成员,并在环境变量未被设置时返回Err
成员。我们使用Result
的is_err
方法来检查其是否是一个 error(也就是环境变量未被设置的情况),这也就意味着我们需要进行一个大小写敏感搜索。如果CASE_INSENSITIVE
环境变量被设置为任何值,is_err
会返回 false 并将进行大小写不敏感搜索。我们并不关心环境变量所设置的值,只关心它是否被设置了,所以检查is_err
而不是unwrap
、expect
或任何我们已经见过的Result
的方法。我们将变量case_sensitive
的值传递给Config
实例这样run
函数可以读取其值并决定是否调用search
或者列表 12-22 中实现的search_case_insensitive
。
让我们试一试吧!首先不设置环境变量并使用查询"to"运行程序,这应该会匹配任何全小写的单词"to"的行:
$ cargo run to poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/greprs to poem.txt`
Are you nobody, too?
How dreary to be somebody!
看起来程序仍然能够工作!现在将CASE_INSENSITIVE
设置为 1 并仍使用相同的查询"to",这回应该得到包含可能有大写字母的"to"的行:
$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/greprs to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
好极了,我们也得到了包含"To"的行!现在greprs
程序可以通过环境变量控制进行大小写不敏感搜索了。现在你知道了如何管理由命令行参数或环境变量设置的选项了!
一些程序允许对相同配置同时使用参数和环境变量。在这种情况下,程序来决定参数和环境变量的优先级。作为一个留给你的测试,尝试同时通过一个命令行参数来控制大小写不敏感搜索,并在程序遇到矛盾值时决定其优先级。
std::env
模块还包含了更多处理环境变量的实用功能;请查看官方文档来了解其可用的功能。