使用环境变量
Working with Environment Variables
我们将通过添加一个额外的功能来改进 minigrep 二进制程序:一个用户可以通过环境变量开启的不区分大小写搜索选项。我们可以将此功能做成命令行选项,并要求用户每次想要应用时都输入它,但通过将其改为环境变量,我们允许用户只需设置一次该环境变量,就能使他们在该终端会话中的所有搜索都不区分大小写。
We’ll improve the minigrep binary by adding an extra feature: an option for
case-insensitive searching that the user can turn on via an environment
variable. We could make this feature a command line option and require that
users enter it each time they want it to apply, but by instead making it an
environment variable, we allow our users to set the environment variable once
and have all their searches be case insensitive in that terminal session.
为不区分大小写的搜索函数编写一个失败测试
Writing a Failing Test for Case-Insensitive Search
我们首先在 minigrep 库中添加一个新的 search_case_insensitive 函数,当环境变量有值时将调用该函数。我们将继续遵循 TDD 流程,所以第一步还是编写一个失败测试。我们将为新的 search_case_insensitive 函数添加一个新测试,并将旧测试从 one_result 重命名为 case_sensitive,以阐明这两个测试之间的区别,如示例 12-20 所示。
We first add a new search_case_insensitive function to the minigrep library
that will be called when the environment variable has a value. We’ll continue
to follow the TDD process, so the first step is again to write a failing test.
We’ll add a new test for the new search_case_insensitive function and rename
our old test from one_result to case_sensitive to clarify the differences
between the two tests, as shown in Listing 12-20.
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)
);
}
}
请注意,我们也修改了旧测试的 contents。我们添加了一行新文本 "Duct tape.",它使用了大写的 D,当我们以区分大小写的方式搜索 "duct" 时,它不应该匹配。以这种方式修改旧测试有助于确保我们不会意外破坏已经实现的区分大小写搜索功能。这个测试现在应该通过,并且在我们开发不区分大小写搜索时应该继续通过。
Note that we’ve edited the old test’s contents too. We’ve added a new line
with the text "Duct tape." using a capital D that shouldn’t match the query
"duct" when we’re searching in a case-sensitive manner. Changing the old test
in this way helps ensure that we don’t accidentally break the case-sensitive
search functionality that we’ve already implemented. This test should pass now
and should continue to pass as we work on the case-insensitive search.
新的不区分大小写搜索测试使用 "rUsT" 作为查询。在我们将要添加的 search_case_insensitive 函数中,查询 "rUsT" 应该匹配包含 "Rust:"(带有大写 R)的行以及 "Trust me." 行,即使这两行的大小写都与查询不同。这是我们的失败测试,由于我们还没有定义 search_case_insensitive 函数,它将无法编译。你可以随意添加一个总是返回空向量的骨架实现,就像我们在示例 12-16 中为 search 函数所做的那样,以观察测试的编译和失败情况。
The new test for the case-insensitive search uses "rUsT" as its query. In
the search_case_insensitive function we’re about to add, the query "rUsT"
should match the line containing "Rust:" with a capital R and match the
line "Trust me." even though both have different casing from the query. This
is our failing test, and it will fail to compile because we haven’t yet defined
the search_case_insensitive function. Feel free to add a skeleton
implementation that always returns an empty vector, similar to the way we did
for the search function in Listing 12-16 to see the test compile and fail.
实现 search_case_insensitive 函数
Implementing the search_case_insensitive Function
示例 12-21 所示的 search_case_insensitive 函数将与 search 函数几乎完全相同。唯一的区别是我们将把 query 和每一行 line 都转换为小写,这样无论输入参数的大小写如何,在检查该行是否包含查询时,它们的大小写都将一致。
The search_case_insensitive function, shown in Listing 12-21, will be almost
the same as the search function. The only difference is that we’ll lowercase
the query and each line so that whatever the case of the input arguments,
they’ll be the same case when we check whether the line contains the query.
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)
);
}
}
首先,我们将 query 字符串转换为小写,并将其存储在同名的新变量中,从而遮盖(shadow)原始的 query。对查询调用 to_lowercase 是必要的,这样无论用户输入的查询是 "rust"、"RUST"、"Rust" 还是 "rUsT",我们都会将查询视为 "rust",从而忽略大小写。虽然 to_lowercase 可以处理基本的 Unicode,但它并不会 100% 准确。如果我们正在编写一个真实的应用程序,我们会想在这里做更多的工作,但本节是关于环境变量而非 Unicode 的,所以我们就此带过。
First, we lowercase the query string and store it in a new variable with the
same name, shadowing the original query. Calling to_lowercase on the query
is necessary so that no matter whether the user’s query is "rust", "RUST",
"Rust", or "rUsT", we’ll treat the query as if it were "rust" and be
insensitive to the case. While to_lowercase will handle basic Unicode, it
won’t be 100 percent accurate. If we were writing a real application, we’d want
to do a bit more work here, but this section is about environment variables,
not Unicode, so we’ll leave it at that here.
注意 query 现在是 String 而不是字符串切片,因为调用 to_lowercase 会创建新数据而不是引用现有数据。以查询是 "rUsT" 为例:该字符串切片不包含供我们使用的小写 u 或 t,因此我们必须分配一个新的包含 "rust" 的 String。现在当我们把 query 作为参数传递给 contains 方法时,我们需要添加一个连字符(&),因为 contains 的签名被定义为接收一个字符串切片。
Note that query is now a String rather than a string slice because calling
to_lowercase creates new data rather than referencing existing data. Say the
query is "rUsT", as an example: That string slice doesn’t contain a lowercase
u or t for us to use, so we have to allocate a new String containing
"rust". When we pass query as an argument to the contains method now, we
need to add an ampersand because the signature of contains is defined to take
a string slice.
接下来,我们在每一行 line 上调用 to_lowercase 来将所有字符转为小写。既然我们已经将 line 和 query 都转换为了小写,无论查询的大小写如何,我们都能找到匹配项。
Next, we add a call to to_lowercase on each line to lowercase all
characters. Now that we’ve converted line and query to lowercase, we’ll
find matches no matter what the case of the query is.
让我们看看这个实现是否通过了测试:
Let’s see if this implementation passes the tests:
$ 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 结构体中添加一个配置选项,用于在区分大小写和不区分大小写的搜索之间切换。添加此字段会导致编译器错误,因为我们还没有在任何地方初始化这个字段:
Great! They passed. Now let’s call the new search_case_insensitive function
from the run function. First, we’ll add a configuration option to the Config
struct to switch between case-sensitive and case-insensitive search. Adding
this field will cause compiler errors because we aren’t initializing this field
anywhere yet:
文件名:src/main.rs 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(())
}
我们添加了持有布尔值的 ignore_case 字段。接下来,我们需要 run 函数检查 ignore_case 字段的值,并据此决定是调用 search 函数还是 search_case_insensitive 函数,如示例 12-22 所示。这仍然无法编译。
We added the ignore_case field that holds a Boolean. Next, we need the run
function to check the ignore_case field’s value and use that to decide
whether to call the search function or the search_case_insensitive
function, as shown in Listing 12-22. This still won’t compile yet.
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(())
}
最后,我们需要检查环境变量。用于处理环境变量的函数位于标准库的 env 模块中,该模块已经包含在 src/main.rs 顶部的作用域内。我们将使用 env 模块中的 var 函数来检查名为 IGNORE_CASE 的环境变量是否设置了任何值,如示例 12-23 所示。
Finally, we need to check for the environment variable. The functions for
working with environment variables are in the env module in the standard
library, which is already in scope at the top of src/main.rs. We’ll use the
var function from the env module to check to see if any value has been set
for an environment variable named IGNORE_CASE, as shown in Listing 12-23.
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(())
}
在这里,我们创建了一个新变量 ignore_case。为了设置它的值,我们调用 env::var 函数并向其传递 IGNORE_CASE 环境变量的名称。如果环境变量被设置为任何值,env::var 函数将返回一个包含该环境变量值的成功 Ok 变体。如果环境变量未设置,它将返回 Err 变体。
Here, we create a new variable, ignore_case. To set its value, we call the
env::var function and pass it the name of the IGNORE_CASE environment
variable. The env::var function returns a Result that will be the
successful Ok variant that contains the value of the environment variable if
the environment variable is set to any value. It will return the Err variant
if the environment variable is not set.
我们使用 Result 上的 is_ok 方法来检查环境变量是否已设置,这意味着程序应该执行不区分大小写的搜索。如果 IGNORE_CASE 环境变量没有设置任何内容,is_ok 将返回 false,程序将执行区分大小写的搜索。我们不关心环境变量的 值,只关心它是否已设置,所以我们检查 is_ok 而不是使用 unwrap、expect 或我们在 Result 上见过的任何其他方法。
We’re using the is_ok method on the Result to check whether the environment
variable is set, which means the program should do a case-insensitive search.
If the IGNORE_CASE environment variable isn’t set to anything, is_ok will
return false and the program will perform a case-sensitive search. We don’t
care about the value of the environment variable, just whether it’s set or
unset, so we’re checking is_ok rather than using unwrap, expect, or any
of the other methods we’ve seen on Result.
我们将 ignore_case 变量中的值传递给 Config 实例,以便 run 函数可以读取该值并决定是调用 search_case_insensitive 还是 search,正如我们在示例 12-22 中实现的那样。
We pass the value in the ignore_case variable to the Config instance so
that the run function can read that value and decide whether to call
search_case_insensitive or search, as we implemented in Listing 12-22.
让我们试一试!首先,我们在不设置环境变量的情况下运行程序,并使用查询 to,这应该匹配任何包含全小写单词 to 的行:
Let’s give it a try! First, we’ll run our program without the environment
variable set and with the query to, which should match any line that contains
the word to in all lowercase:
$ 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:
Looks like that still works! Now let’s run the program with IGNORE_CASE set
to 1 but with the same query to:
$ IGNORE_CASE=1 cargo run -- to poem.txt
如果你正在使用 PowerShell,你需要将设置环境变量和运行程序作为单独的命令:
If you’re using PowerShell, you will need to set the environment variable and run the program as separate commands:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
这将使 IGNORE_CASE 在当前 shell 会话的剩余时间内保持有效。可以使用 Remove-Item cmdlet 取消设置:
This will make IGNORE_CASE persist for the remainder of your shell session.
It can be unset with the Remove-Item cmdlet:
PS> Remove-Item Env:IGNORE_CASE
我们应该会得到包含 to 的行,且其中可能包含大写字母:
We should get lines that contain to that might have uppercase letters:
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
太棒了,我们也得到了包含 To 的行!我们的 minigrep 程序现在可以执行由环境变量控制的不区分大小写搜索。现在你知道了如何管理通过命令行参数或环境变量设置的选项。
Excellent, we also got lines containing To! Our minigrep program can now do
case-insensitive searching controlled by an environment variable. Now you know
how to manage options set using either command line arguments or environment
variables.
有些程序允许对同一配置使用参数 和 环境变量。在这些情况下,程序会决定其中一个具有更高的优先级。作为你自己的另一个练习,尝试通过命令行参数或环境变量来控制大小写敏感度。决定如果程序在运行时一个设置为区分大小写而另一个设置为忽略大小写,是命令行参数还是环境变量应该具有优先权。
Some programs allow arguments and environment variables for the same configuration. In those cases, the programs decide that one or the other takes precedence. For another exercise on your own, try controlling case sensitivity through either a command line argument or an environment variable. Decide whether the command line argument or the environment variable should take precedence if the program is run with one set to case sensitive and one set to ignore case.
std::env 模块包含更多用于处理环境变量的实用功能:查看其文档以了解有哪些可用功能。
The std::env module contains many more useful features for dealing with
environment variables: Check out its documentation to see what is available.