按照文档来造个 grep 的 CLI 工具,适当脱离文档加一些自己想要的功能。
先创建个项目叫 minigrep
$ cargo new minigre
再在 src目录下加个 lib.rs用来写核心逻辑,然后在 main.rs中只是使用 lib 中提供的函数进行逻辑组合。
Config
在 lib 中写个 Config 结构用来封装 grep 的关键词和文件名,并且暴露个 run 的执行方法,入参就是 Config
// src/lib.rspub 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.rsuse 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.rspub 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 ,第二个是 valuefor (i, str) in contents.lines().enumerate() {// 如果包含 query 信息,就收集内容if str.contains(query) {list.push(SearchResult {line: i + 1,content: str,});}}list}
但是只是查出来还不够,还想优化一下,想实现类似于下面这种效果,也就是能标记出匹配的内容,并且能不区分大小写。
Search 123 in search.txt1. | 123| ^^^2. | 123| ^^^3. | aaaaaa 123 bb| ^^^
优化匹配
原来是用字符串的 contains 方法,如果需要实现上面的效果,可以改成 match_indices 方法,这个方法能返回匹配的内容和匹配了第几个字符。
然后在 SearchResult里再新增个 underline的内容,用来存储下划线信息。
// src/lib.rspub 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.rspub 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.");}}
