本章我们将会构建一个与文件和命令行输入/输出交互的命令行工具来练习已经学过的Rust技能
Rust的运行速度、安全性、单二进制文件输出和跨平台支持使其成为创建命令行程序的绝佳选择,所以我们将创建一个我们自己的经典命令行工具grep(globally research a regular expression and print)
Grep最简单的使用场景是在特定文件中搜索指定字符串
在这个过程中,我们的命令行工具会用到终端功能,读取环境变量来使得用户可以配置工具,打印到标准错误控制流而不是标准输出。这样用户可以选择将成功输出重定向到文件中同时也能够在屏幕上显示错误信息
并且我们在本章会涉及代码组织、vector和字符串、错误处理、trait和生命周期以及测试,另外闭包、迭代器我们也会了解一点点
本节我们将会遵循测试驱动开发(TDD)的模式来逐步增加minigrep的搜索逻辑,这是一个软件开发技术,它遵循一下步骤
1.编写一个失败的测试,运行它并确保它失败如你期望
2.编写或者修改代码使得测试通过
3.重构之前的代码,并确保代码仍能通过
4.从步骤1开始重复
编写失败测试
我们刚刚构建的minigrep能够成功运行,现在我们来在lib.rs中编写一个测试类似功能的测试
#[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)
);
}
}
断言中比较的是一个字符串vec和 search函数的返回结果,因此我们再来构建search函数
pub fn search<'a>(query:&str,contents:&'a str)->Vec<&'a str>{
vec![]
}
search函数签名中需要两个参数,函数的返回值是一个字符串的Vec,并且在这里我们使用了显示生命周期‘a,因为我们想告诉编译器 contents和vector中的字符串生命周期一样长,无论传入的参数是什么,我们都返回一个空的vec,我们想让测试失败
现在我们运行一下测试
running 1 test
thread 'tests::one_result' panicked at 'assertion failed: `(left == right)`
left: `["safe,fast,productive."]`,
right: `[]`', src/lib.rs:40:12
test tests::one_result ... FAILED
failures:
failures:
tests::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
我们看到测试结果如我们所期望的一般失败
编写使测试通过的代码
现在就让我们来根据这个方向来编写能通过测试的代码
目前我们之所以会失败,是因为我们返回了一个空的vec,和assert_eq!用来比较的对不上,我们解决这个问题的思路如下:
1.遍历内容的每一行文本
2.查看这一行的内容,看看其是否包含要搜索的字符串
3.如果有,将其加入列表的返回值中
4.如果没有,什么也不做
5.返回匹配到的结果列表
Rust本身是有一个方法叫lines,可以用来一行一行遍历文本中的字符串,返回的是一个迭代器
pub fn search<'a>(query:&str,contents:&'a str)->Vec<&'a str>{
for line in contents.lines() {
//do something with line
}
}
用查询字符串搜索每一行
现在我们在遍历的行里查询特定字段,当然,非常幸运,Rust也有这个方法
pub fn search<'a>(query:&str,contents:&'a str)->Vec<&'a str>{
for line in contents.lines() {
if line.contains(query){
//do something with line
}
}
}
我们把符合我们查找条件的行存储下来,而此时我们的search也构建完毕了
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
}
我们来看看结果
warning: `minigrep` (lib test) generated 1 warning
Finished test [unoptimized + debuginfo] target(s) in 0.74s
Running unittests (target/debug/deps/minigrep-d92bb246cb5b8bbe)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
非常完美,我们的测试函数构建好了,我们接下来用它!
在run函数中使用search函数
pub fn run(config:Config)->Result<(),Box>{
let contents = fs::read_to_string(config.filename)?;
for line in search(&config.query, &contents){
println!("{}",line)
}
Ok(())
}
我们来使用它在poem.txt中搜索特定的字符串
% cargo run frog poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
Running `/Users/xxx/minigrep/target/debug/minigrep frog poem.txt`
Searching for frog
In file poem.txt
How public, like a frog
% cargo run body poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `/Users/qinjianquan/minigrep/target/debug/minigrep body poem.txt`
Searching for body
In file poem.txt
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
% cargo run no-input poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.04s
Running `/Users/qinjianquan/minigrep/target/debug/minigrep no-input poem.txt`
Searching for no-input
In file poem.txt
我们在这里搜索了一个、多个以及无匹配的结果,程序如我们的期望在运行,非常不错