[Rust Guide] 12.7. Using Environment Variables

[Rust Guide] 12.7. Using Environment Variables

12.7.0 Before We Begin

Chapter 12 builds a sample project: a command-line program. The program is grep (Global Regular Expression Print), a tool for global regular-expression searching and output. Its function is to search for specified text in a specified file. This project is divided into these steps:

  • Receiving command-line arguments
  • Reading files
  • Refactoring: improving modules and error handling
  • Using TDD (test-driven development) to develop library functionality
  • Using environment variables (this article)
  • Writing error messages to standard error instead of standard output

12.7.0 开始之前

第 12 章构建了一个示例项目:一个命令行程序。该程序是 grep (Global Regular Expression Print),一个用于全局正则表达式搜索和输出的工具。它的功能是在指定文件中搜索指定的文本。该项目分为以下几个步骤:

  • 接收命令行参数
  • 读取文件
  • 重构:改进模块和错误处理
  • 使用 TDD(测试驱动开发)开发库功能
  • 使用环境变量(本文)
  • 将错误消息写入标准错误而不是标准输出

12.7.1 Review

Here is all the code written up to the previous article.

12.7.1 回顾

以下是截至上一篇文章所编写的所有代码。

lib.rs:

use std::error::Error;
use std::fs;

pub struct Config {
    pub query: String,
    pub filename: String,
}

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();
        Ok(Config { query, filename, })
    }
}

pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    let contents = fs::read_to_string(config.filename)?;
    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 one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";
        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}

main.rs:

use std::env;
use std::process;
use minigrep::Config;

fn main() {
    let args: Vec<String> = env::args().collect();
    let config = Config::new(&args).unwrap_or_else(|err| {
        println!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

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

In these earlier sections, we completed the move of the business logic into lib.rs. That is very helpful for writing tests, because the logic in lib.rs can be called directly with different arguments without running the program from the command line, and its return values can be checked. In other words, we can test the business logic directly.

在前面的章节中,我们完成了将业务逻辑移动到 lib.rs 的工作。这对编写测试非常有帮助,因为 lib.rs 中的逻辑可以直接用不同的参数调用,而无需从命令行运行程序,并且可以检查其返回值。换句话说,我们可以直接测试业务逻辑。


12.7.2 What Is TDD?

TDD is short for Test-Driven Development, and it usually follows these steps:

  1. Write a failing test, run it, and make sure it fails for the expected reason.
  2. Write or modify just enough code to make the new test pass.
  3. Refactor the code you just added or changed, and make sure the tests still pass.
  4. Return to step 1 and continue.

TDD is only one of many software development methods, but it can guide and support code design. Writing tests first and then writing code that passes them also helps maintain a high level of test coverage during development. In this article, we will use TDD to implement the search logic for the program: search for a specified string in the file contents and put the matching lines into a list. This function will be called search_case_insensitive.

12.7.2 什么是 TDD?

TDD 是测试驱动开发(Test-Driven Development)的缩写,它通常遵循以下步骤:

  1. 编写一个失败的测试,运行它,并确保它因预期的原因失败。
  2. 编写或修改刚好足够的代码以使新测试通过。
  3. 重构刚刚添加或更改的代码,并确保测试仍然通过。
  4. 返回第 1 步并继续。

TDD 只是众多软件开发方法中的一种,但它可以指导和支持代码设计。先编写测试,然后再编写通过测试的代码,也有助于在开发过程中保持高水平的测试覆盖率。在本文中,我们将使用 TDD 来实现程序的搜索逻辑:在文件内容中搜索指定的字符串,并将匹配的行放入列表中。此函数将命名为 search_case_insensitive


12.7.3 Write a Failing Test

Let’s start by naming the case-insensitive function search_case_insensitive. First, modify the test module so that it contains a case-sensitive test and a case-insensitive test:

12.7.3 编写一个失败的测试

让我们从命名不区分大小写的函数 search_case_insensitive 开始。首先,修改测试模块,使其包含一个区分大小写的测试和一个不区分大小写的测试:

#[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)
        );
    }
}

Then write the body of search_case_insensitive:

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}
  • To make this function callable from outside, it must be declared public with pub.
  • The function needs a lifetime annotation because it has multiple non-self parameters, and Rust cannot determine which parameter’s lifetime should match the lifetime of the return value.
  • The elements in the returned Vector are string slices taken from contents, so the return value should have the same lifetime as contents; that is why both are annotated with the same lifetime 'a, while query does not need a lifetime annotation.
  • The function body only needs to compile, because the first step of TDD is to write a test that fails, so failure is the desired outcome.

At this point, running the tests will definitely fail, but that is fine. This is exactly what the first TDD step is supposed to produce.

然后编写 search_case_insensitive 的函数体:

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}
  • 为了使该函数能从外部调用,必须使用 pub 将其声明为公共函数。
  • 该函数需要生命周期标注,因为它有多个非 self 参数,Rust 无法确定哪个参数的生命周期应与返回值的生命周期匹配。
  • 返回的 Vector 中的元素是从 contents 中获取的字符串切片,因此返回值应与 contents 具有相同的生命周期;这就是为什么两者都标注了相同的生命周期 'a,而 query 不需要生命周期标注。
  • 函数体只需要能够编译即可,因为 TDD 的第一步是编写一个失败的测试,所以失败是预期的结果。

此时,运行测试肯定会失败,但这没关系。这正是 TDD 第一步应该产生的结果。


12.7.4 Write or Modify Just Enough Code for the New Test to Pass

The code for search_case_insensitive is very similar to search, so only a few small changes are needed. The logic is simple: lowercase the query and compare it against lowercase versions of the text:

12.7.4 编写或修改刚好足够的代码以使新测试通过

search_case_insensitive 的代码与 search 非常相似,因此只需要进行一些小的修改。逻辑很简单:将查询字符串转换为小写,并将其与文本的小写版本进行比较:

pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();
    let query = query.to_lowercase();
    for line in contents.lines() {
        if line.to_lowercase().contains(&query) {
            results.push(line);
        }
    }
    results
}
  • The to_lowercase method converts a string to all lowercase.
  • The result of to_lowercase is a String, so the new query is an owned String rather than &str.
  • Inside the loop, we use &query because contains does not accept String directly, so we pass a reference.

Run the tests again:

$ cargo test
...
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
  • to_lowercase 方法将字符串转换为全小写。
  • to_lowercase 的结果是一个 String,因此新的 query 是一个拥有的 String 而不是 &str
  • 在循环内部,我们使用 &query,因为 contains 不直接接受 String,所以我们传递一个引用。

再次运行测试: (输出结果显示测试通过)