Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12.7 使用环境变量

12.7.0. 写在正文之前

第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。

这个项目分为这么几步:

12.7.1. 回顾

以下是截止到上一篇文章为止所写出的全部代码。

lib.rs:

#![allow(unused)]
fn main() {
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);
    }
}

本文,我们将通过添加一个额外的功能来改进minigrep:用户可以通过环境变量打开不区分大小写搜索的选项。我们可以将此功能设置为命令行选项,并要求用户每次希望应用时输入它,但通过将其设置为环境变量,我们允许用户设置环境变量一次,并在那个终端会话中使所有搜索都不区分大小写。

12.7.2. 继续用TDD:不区分大小写搜索

我们再次遵循TDD的步骤:

  • 编写一个会失败的测试,运行该测试,并确保它是按照预期的原因失败
  • 编写或修改刚好足够的代码,让新测试通过
  • 重构刚刚添加或修改的代码,确保测试会通过
  • 返回步骤1,继续

本篇文章会继续用TDD来实现不区分大小写的搜索。新函数会被命名为search_case_insensitive,之后再用环境变量来控制是否启用它。

12.7.3. 编写会失败的测试

这个对大小写不敏感的函数先给它起个名叫做search_case_insensitive

先把测试模块改一下,改出一个对大小写敏感的测试函数和不敏感的测试函数:

#![allow(unused)]
fn main() {
#[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)
        );
    }
}
}

然后再写search_case_insensitive函数的具体内容:

#![allow(unused)]
fn main() {
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. 编写或修改刚好足够的代码,让新测试通过

其实search_case_insensitive的代码与search的大部分都差不多,只需要做一些小修改即可。逻辑很好想,就是把关键词变成小写,再与文本各行的小写形式进行比较:

#![allow(unused)]
fn main() {
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
}
}
  • to_lowercase方法可以把字符串变成全小写
  • to_lowercase转换后的结果是String,变量拥有所有权,也就是新的queryString而不是&str。在循环中的if语句使用的是&query,因为contains方法不接受String所以得传引用进去

再跑一下测试:

$ cargo test
   Compiling minigrep v0.1.0 (/tmp/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running unittests src/lib.rs (target/debug/deps/minigrep-dfdfbb86b622af32)

running 2 tests
test tests::case_sensitive ... ok
test tests::case_insensitive ... 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-4c31ade9c6771135)

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

两个测试都通过了。

12.7.5. 在run函数中使用此函数

这个函数没问题了,就可以在run函数中调用了。

但是首先得先为Config结构体添加一个字段来作为使用普通的search还是对大小写不敏感的search_case_insensitive的依据:

#![allow(unused)]
fn main() {
pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}
}

修改run函数让它判断配置:

#![allow(unused)]
fn main() {
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(())
}
}

Config上的new这个构造器也得改,根据环境变量给case_sensitive这个字段赋值:

#![allow(unused)]
fn main() {
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 = std::env::var("IGNORE_CASE").is_err();
        Ok(Config {
            query,
            filename,
            case_sensitive,
        })
    }
}
}

这里使用了std::env::var这个函数(当然也可以先把std::env导入作用域,再使用env::var),它的参数是这个环境变量的名称(按惯例全大写),这里我写的是IGNORE_CASE,中文翻译过来就是忽略大小写。这种环境变量只要出现就认为不区分大小写,不出现就认为区分大小写。

std::env::var的返回值是Result类型的,如果这个IGNORE_CASE环境变量被设置了,返回包含环境变量值的Ok(String),反之返回Err(std::env::VarError)

这里std::env::var后面还跟了is_err这个方法:如果返回值是Err变体,is_err就返回true,于是case_sensitive被赋为true;反之(环境变量已设置)则赋false

PS:说实话,这个小程序写成这个B样也是为了教学的无奈之举,我看到一半我都被这个代码量气笑了,真正写的时候没必要写得这么一板一眼的

12.7.6. 整体代码与试运行

写了这么多,看看截止到目前的所有代码。

lib.rs:

#![allow(unused)]
fn main() {
use std::error::Error;
use std::fs;

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

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 = std::env::var("IGNORE_CASE").is_err();
        Ok(Config {
            query,
            filename,
            case_sensitive,
        })
    }
}

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(())
}

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 mut results = Vec::new();
    let query = query.to_lowercase();
    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)
        );
    }
}
}

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

来试运行一下:

首先,我们将在不设置环境变量的情况下运行程序,并使用查询to,该查询应与包含全部小写单词to的任何行匹配:

$ cargo run -- to poem.txt
   Compiling minigrep v0.1.0 (/tmp/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
     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

会得到:

Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!

没有任何问题。

注意,如果你在powershell中,设置环境变量得这么写:

PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt

这会使这个环境变量在这个会话中一直存在,如果要去掉这个环境变量,写:

PS> Remove-Item Env:IGNORE_CASE