12.7.0. 写在正文之前
第12章要做一个实例的项目——一个命令行程序。这个程序是一个grep
(Global Regular Expression Print),是一个全局正则搜索和输出的工具。它的功能是在指定的文件中搜索出指定的文字。
这个项目分为这么几步:
- 接收命令行参数
- 读取文件
- 重构:改进模块和错误处理
- 使用TDD(测试驱动开发)开发库功能
- 使用环境变量(本文)
- 将错误信息写入标准错误而不是标准输出
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
12.7.1. 回顾
以下是截止到上一篇文章为止所写出的全部代码。
lib.rs
:
rust">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
:
rust">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. 编写不区分大小写的search
函数
这里不区分大小写的功能是通过环境变量来实现的,当然也可使用参数来实现,但使用环境变量的好处是只需要配置一次就可以在整个终端的会话中一直保持有效。
对于这个功能,我们也使用TDD(测试驱动开发)流程来开发:
- 编写一个会失败的测试,运行该测试,并确保它是按照预期的原因失败
- 编写或修改刚好足够的代码,让新测试通过
- 重构刚刚添加或修改的代码,确保测试会通过
- 返回步骤1,继续
1. 编写一个会失败的测试
这个对大小写不敏感的函数先给它起个名叫做search_case_insensitive
先把测试模块改一下,改出一个对大小写敏感的测试函数和不敏感的测试函数:
rust">#[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
函数的具体内容:
rust">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第一步想要的
2. 编写或修改刚好足够的代码,让新测试通过
其实search_case_insensitive
的代码与search
的大部分都差不多,只需要做一些小修改即可。逻辑很好想,就是把关键词和文本内容都变成全小写即可:
rust">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.to_lowercase().lines() { if line.contains(&query) { results.push(line); } } results
}
to_lowercase
方法可以把字符串变成全小写to_lowercase
转换后的结果是String
,变量拥有所有权,也就是新的query
是String
而不是&str
。在循环中的if
语句使用的是&query
,因为contains
方法不接受String
所以得传引用进去。
再跑一下测试:
$ cargo testCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33sRunning unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... oktest result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sRunning unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)running 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sDoc-tests minigreprunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
两个测试都通过了
3. 在run
函数中使用此函数
这个函数没问题了,就可以在run
函数中调用了。
但是首先得先为Config
结构体添加一个字段来作为使用普通的search
还是对大小写不敏感的search_case_insensitive
的依据:
rust">pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool,
}
修改run
函数让它判断配置:
rust">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
这个字段赋值:
rust">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("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive}) }
}
这里使用了std::env::var
这个函数(当然也可以先把std::env
导入作用域,再使用env::var
),它的参数是这个环境变量的名称(按惯例全大写),这里我写的是CASE_INSENSITIVE,中文翻译过来就是大小写不敏感。这种环境变量只要出现就认为不区分大小写,不出现就认为区分大小写。
std::env::var
的返回值是Result
类型的,如果这个CASE_INSENSITIVE环境变量被设置了,返回包含环境变量值的Ok(String)
,反之返回Err(std::env::VarError)
。
这里std::env::var
后面还跟了is_err
这个方法,如果is_err
是Err
变体,就会返回赋true
给变量case_sensitive
,反之就是赋false
。
PS:说实话,这个小程序写成这个B样也是为了教学的无奈之举,我看到一半我都被这个代码量气笑了,真正写的时候没必要写得这么一板一眼的
12.7.3. 整体代码与试运行
写了这么多,看看截止到目前的所有代码。
lib.rs
:
rust">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("CASE_INSENSITIVE").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.to_lowercase().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) ); }
}
main.rs
:
rust">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.txtCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0sRunning `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