按照文档来造个 grep 的 CLI 工具,适当脱离文档加一些自己想要的功能。

先创建个项目叫 minigrep

  1. $ cargo new minigre

再在 src目录下加个 lib.rs用来写核心逻辑,然后在 main.rs中只是使用 lib 中提供的函数进行逻辑组合。

Config

在 lib 中写个 Config 结构用来封装 grep 的关键词和文件名,并且暴露个 run 的执行方法,入参就是 Config

  1. // src/lib.rs
  2. pub struct Config {
  3. pub query: String,
  4. pub filename: String,
  5. }
  6. impl Config {
  7. pub fn new(args: &[String]) -> Result<Config, &str> {
  8. if args.len() < 3 {
  9. Err("not engough arguments")
  10. } else {
  11. Ok(Config {
  12. query: args[1].clone(),
  13. filename: args[2].clone(),
  14. })
  15. }
  16. }
  17. }
  18. pub fn run(config: Config) -> Result<String, Box<dyn Error>> {
  19. // ...
  20. }

获取 args

在 main 中引入 lib 中的 Config 以及 run 方法,通过 env 这个库获取 args

  1. // src/main.rs
  2. use std::{env, process};
  3. use minigrep::{Config, run};
  4. fn main() {
  5. let args: Vec<String> = env::args().collect();
  6. let config = Config::new(&args).unwrap_or_else(|err| {
  7. println!("parse arguments failed with reason: {}", err);
  8. process::exit(1);
  9. });
  10. let result = run(config);
  11. if let Err(e) = result {
  12. println!("execute failed with {:?}", e)
  13. } else if let Ok(str) = result {
  14. println!("{}", str);
  15. }
  16. }

可以通过 process:exit(1) 来退出进程,类似于 node 中的 process.exit

文件读取

前面的文档就有学过文件读取,可以直接用 fs 这个库的 api 操作,通过 or_else 可以对错误信息做预处理。

  1. // src/lib.rs
  2. pub fn run(config: Config) -> Result<String, Box<dyn Error>> {
  3. let contents = fs::read_to_string(&config.filename)
  4. .or_else(|err| {
  5. if err.kind() == ErrorKind::NotFound {
  6. Err(format!("failed to read file: {}", config.filename))
  7. } else {
  8. Err(format!("{:?}", err))
  9. }
  10. })?;
  11. // do other things
  12. }

字符操作

前面从文件读出内容后,通过 search 方法查询包含关键词的那一行,封装一个 SearchResult 的结构,用于存储检索到的内容,以及行数。

  1. pub struct SearchResult<'a> {
  2. pub line: usize,
  3. pub content: &'a str,
  4. }
  5. pub fn search<'a>(query: &String, contents: &'a String) -> Vec<SearchResult<'a>> {
  6. let mut list = Vec::new();
  7. // enumerate 可以返回一个 tuple ,第一个是 index ,第二个是 value
  8. for (i, str) in contents.lines().enumerate() {
  9. // 如果包含 query 信息,就收集内容
  10. if str.contains(query) {
  11. list.push(SearchResult {
  12. line: i + 1,
  13. content: str,
  14. });
  15. }
  16. }
  17. list
  18. }

但是只是查出来还不够,还想优化一下,想实现类似于下面这种效果,也就是能标记出匹配的内容,并且能不区分大小写。

  1. Search 123 in search.txt
  2. 1. | 123
  3. | ^^^
  4. 2. | 123
  5. | ^^^
  6. 3. | aaaaaa 123 bb
  7. | ^^^

优化匹配

原来是用字符串的 contains 方法,如果需要实现上面的效果,可以改成 match_indices 方法,这个方法能返回匹配的内容和匹配了第几个字符。

然后在 SearchResult里再新增个 underline的内容,用来存储下划线信息。

  1. // src/lib.rs
  2. pub struct SearchResult<'a> {
  3. pub line: usize,
  4. pub content: &'a str,
  5. pub underline: String,
  6. }
  7. pub fn search<'a>(query: &String, contents: &'a String) -> Vec<SearchResult<'a>> {
  8. let mut list = Vec::new();
  9. // 统一小写
  10. let lower_query = query.to_lowercase();
  11. for (i, str) in contents.lines().enumerate() {
  12. // 先转成小写再去匹配,to_lowercase 会将字符拷贝一份
  13. let lower_str = str.to_lowercase();
  14. // 进行匹配
  15. let matches: Vec<(usize, &str)> = lower_str.match_indices(&lower_query).collect();
  16. // 如果 match 数量大于 0 ,就说明匹配中了
  17. if matches.len() > 0 {
  18. list.push(SearchResult {
  19. line: i + 1,
  20. content: str,
  21. underline: create_underline(&matches)
  22. });
  23. }
  24. }
  25. list
  26. }
  27. // 根据 match 的结果,返回 underline 内容
  28. pub fn create_underline(matches: &Vec<(usize, &str)>) -> String {
  29. let mut underline = String::new();
  30. let mut index = 0;
  31. for (match_index, match_str) in matches {
  32. let space_len = match_index - index;
  33. // 根据匹配结果,生成 ^^^ 的内容。
  34. underline.push_str(&format!("{}{}", " ".repeat(space_len), "^".repeat(match_str.len())));
  35. index = *match_index + match_str.len();
  36. }
  37. underline
  38. }

逻辑组合

再在 run 函数中调用 search 方法,拿到 SearchResult列表,再组合成要打印的信息返回。

  1. // src/lib.rs
  2. pub fn run(config: Config) -> Result<String, Box<dyn Error>> {
  3. // ...snip...
  4. let result = search(&config.query, &contents);
  5. // 格式化输出结果
  6. let mut str_result = String::new();
  7. str_result.push_str(&format!("\n Search {} in {}\n", config.query, config.filename));
  8. for r in result {
  9. let prefix = format!("{}{}", " ".repeat(6 - r.line.to_string().len()), r.line);
  10. str_result.push_str(&format!("\n {}. | {}\n", prefix, r.content));
  11. str_result.push_str(&format!(" {} | {}", " ".repeat(prefix.len()), r.underline));
  12. }
  13. str_result.push_str("\n");
  14. Ok(str_result)
  15. }

因为在 main.rs 中是直接打印 run函数返回的字符信息,然后测试一下

aaa 为关键词,search.txt 为检索的文本。

  1. $ cargo run aaa search.txt

效果如下
image.png

单元测试

写代码必须得测试驱动,所以单测不能忘记,简单写两个先 …

  1. // src/lib.rs
  2. // ...snip...
  3. #[cfg(test)]
  4. mod tests {
  5. use super::*;
  6. #[test]
  7. fn config_should_works() {
  8. let args = [ String::from("a") ];
  9. let conf = Config::new(&args);
  10. match conf {
  11. Err(e) => {
  12. assert_eq!(e, "not engough arguments");
  13. },
  14. _ => panic!("should throw error")
  15. }
  16. }
  17. #[test]
  18. fn should_search_without_error() {
  19. let mut query = String::from("duct");
  20. let contents = "
  21. Rust:
  22. safe, fast, productive.
  23. Pick three.
  24. ".to_string();
  25. let result = search(&query, &contents);
  26. assert_eq!(result.len(), 1);
  27. assert_eq!(result[0].line, 3);
  28. assert_eq!(result[0].content, "safe, fast, productive.");
  29. assert_eq!(result[0].underline, " ^^^^");
  30. query.clear();
  31. query.push_str("u");
  32. let result2 = search(&query, &contents);
  33. assert_eq!(result2.len(), 2);
  34. assert_eq!(result2[0].line, 2);
  35. assert_eq!(result2[0].content, "Rust:");
  36. assert_eq!(result2[1].line, 3);
  37. assert_eq!(result2[1].content, "safe, fast, productive.");
  38. }
  39. }