按照文档来造个 grep
的 CLI 工具,适当脱离文档加一些自己想要的功能。
先创建个项目叫 minigrep
$ cargo new minigre
再在 src
目录下加个 lib.rs
用来写核心逻辑,然后在 main.rs
中只是使用 lib 中提供的函数进行逻辑组合。
Config
在 lib 中写个 Config 结构用来封装 grep 的关键词和文件名,并且暴露个 run
的执行方法,入参就是 Config
// src/lib.rs
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &str> {
if args.len() < 3 {
Err("not engough arguments")
} else {
Ok(Config {
query: args[1].clone(),
filename: args[2].clone(),
})
}
}
}
pub fn run(config: Config) -> Result<String, Box<dyn Error>> {
// ...
}
获取 args
在 main 中引入 lib 中的 Config 以及 run 方法,通过 env
这个库获取 args
// src/main.rs
use std::{env, process};
use minigrep::{Config, run};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
println!("parse arguments failed with reason: {}", err);
process::exit(1);
});
let result = run(config);
if let Err(e) = result {
println!("execute failed with {:?}", e)
} else if let Ok(str) = result {
println!("{}", str);
}
}
可以通过 process:exit(1)
来退出进程,类似于 node 中的 process.exit
。
文件读取
前面的文档就有学过文件读取,可以直接用 fs
这个库的 api 操作,通过 or_else
可以对错误信息做预处理。
// src/lib.rs
pub fn run(config: Config) -> Result<String, Box<dyn Error>> {
let contents = fs::read_to_string(&config.filename)
.or_else(|err| {
if err.kind() == ErrorKind::NotFound {
Err(format!("failed to read file: {}", config.filename))
} else {
Err(format!("{:?}", err))
}
})?;
// do other things
}
字符操作
前面从文件读出内容后,通过 search 方法查询包含关键词的那一行,封装一个 SearchResult
的结构,用于存储检索到的内容,以及行数。
pub struct SearchResult<'a> {
pub line: usize,
pub content: &'a str,
}
pub fn search<'a>(query: &String, contents: &'a String) -> Vec<SearchResult<'a>> {
let mut list = Vec::new();
// enumerate 可以返回一个 tuple ,第一个是 index ,第二个是 value
for (i, str) in contents.lines().enumerate() {
// 如果包含 query 信息,就收集内容
if str.contains(query) {
list.push(SearchResult {
line: i + 1,
content: str,
});
}
}
list
}
但是只是查出来还不够,还想优化一下,想实现类似于下面这种效果,也就是能标记出匹配的内容,并且能不区分大小写。
Search 123 in search.txt
1. | 123
| ^^^
2. | 123
| ^^^
3. | aaaaaa 123 bb
| ^^^
优化匹配
原来是用字符串的 contains
方法,如果需要实现上面的效果,可以改成 match_indices
方法,这个方法能返回匹配的内容和匹配了第几个字符。
然后在 SearchResult
里再新增个 underline
的内容,用来存储下划线信息。
// src/lib.rs
pub struct SearchResult<'a> {
pub line: usize,
pub content: &'a str,
pub underline: String,
}
pub fn search<'a>(query: &String, contents: &'a String) -> Vec<SearchResult<'a>> {
let mut list = Vec::new();
// 统一小写
let lower_query = query.to_lowercase();
for (i, str) in contents.lines().enumerate() {
// 先转成小写再去匹配,to_lowercase 会将字符拷贝一份
let lower_str = str.to_lowercase();
// 进行匹配
let matches: Vec<(usize, &str)> = lower_str.match_indices(&lower_query).collect();
// 如果 match 数量大于 0 ,就说明匹配中了
if matches.len() > 0 {
list.push(SearchResult {
line: i + 1,
content: str,
underline: create_underline(&matches)
});
}
}
list
}
// 根据 match 的结果,返回 underline 内容
pub fn create_underline(matches: &Vec<(usize, &str)>) -> String {
let mut underline = String::new();
let mut index = 0;
for (match_index, match_str) in matches {
let space_len = match_index - index;
// 根据匹配结果,生成 ^^^ 的内容。
underline.push_str(&format!("{}{}", " ".repeat(space_len), "^".repeat(match_str.len())));
index = *match_index + match_str.len();
}
underline
}
逻辑组合
再在 run 函数中调用 search 方法,拿到 SearchResult
列表,再组合成要打印的信息返回。
// src/lib.rs
pub fn run(config: Config) -> Result<String, Box<dyn Error>> {
// ...snip...
let result = search(&config.query, &contents);
// 格式化输出结果
let mut str_result = String::new();
str_result.push_str(&format!("\n Search {} in {}\n", config.query, config.filename));
for r in result {
let prefix = format!("{}{}", " ".repeat(6 - r.line.to_string().len()), r.line);
str_result.push_str(&format!("\n {}. | {}\n", prefix, r.content));
str_result.push_str(&format!(" {} | {}", " ".repeat(prefix.len()), r.underline));
}
str_result.push_str("\n");
Ok(str_result)
}
因为在 main.rs
中是直接打印 run
函数返回的字符信息,然后测试一下
aaa 为关键词,search.txt 为检索的文本。
$ cargo run aaa search.txt
单元测试
写代码必须得测试驱动,所以单测不能忘记,简单写两个先 …
// src/lib.rs
// ...snip...
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_should_works() {
let args = [ String::from("a") ];
let conf = Config::new(&args);
match conf {
Err(e) => {
assert_eq!(e, "not engough arguments");
},
_ => panic!("should throw error")
}
}
#[test]
fn should_search_without_error() {
let mut query = String::from("duct");
let contents = "
Rust:
safe, fast, productive.
Pick three.
".to_string();
let result = search(&query, &contents);
assert_eq!(result.len(), 1);
assert_eq!(result[0].line, 3);
assert_eq!(result[0].content, "safe, fast, productive.");
assert_eq!(result[0].underline, " ^^^^");
query.clear();
query.push_str("u");
let result2 = search(&query, &contents);
assert_eq!(result2.len(), 2);
assert_eq!(result2[0].line, 2);
assert_eq!(result2[0].content, "Rust:");
assert_eq!(result2[1].line, 3);
assert_eq!(result2[1].content, "safe, fast, productive.");
}
}