Skip to content

实现一个 minigrep

能够这样使用: cargo run hello ./readme.txt -I

  • hello: 是要搜索的内容
  • readme.txt: 是要搜索的文件路径
  • -I: 是否忽略大小写

创建项目

sh
cargo new minigrep
cargo bininstall cargo-watch

目录结构

txt
.
├── Cargo.lock
├── Cargo.toml
├── readme.txt  // 用来匹配的文件
└── src
    ├── lib.rs
    └── main.rs

创建 readme.txt, 并输入文件内容:

txt
Alias quia laudantium.
Corporis rem assumenda.
hello In laborum sed.
Id consectetur eius fugiat
Labore rerum blanditiis
Hello, case_sensitive

源码实现

rust
use std::env;
use std::process;
use minigrep::*;

fn main() {
    // 获取运行程序时的参数 cargo run hello ./readme.txt
    // hello 和 readme.txt 用这个 API 就可以获取到
    let args: Vec<String> = env::args().collect();

    // unwrap_or_else:
    // 如果返回 Ok 那么就直接将 Ok(x) 的 x 返回
    // 如狗返回 Err 那么就将执行这个匿名函数(闭包)
    let config = Config::new(&args).unwrap_or_else(|err|{
        // println 是将信息输出到标准输出
        // eprintln 是将信息输出到标准错误 stderr
        eprintln!("Failed to parse arguments: {}", err);
        process::exit(1);
    });

    // 如果 run 函数出现错误, 立即停止执行
    if let Err(e) = run(config) {
        eprintln!("Application error: {}", e);
        process::exit(1);
    }
}
rust
use std::fs;
use std::error::Error;

// 配置
pub struct Config {
    query: String,
    filename: String,
    case_sensitive: bool,
}

// 实例化 Config 结构体, 处理参数
impl Config {
    pub fn new(args: &[String]) -> Result<Config, &'static str> {
        let len = args.len();

        // 如果参数有误, 我直接执行不给参数 cargo run
        if len < 3 {
            return Err("Not enough parameters");
        }

        // 实例化配置结构体
        let mut config = Self {
            query: args[1].clone(),
            filename: args[2].clone(),
            case_sensitive: true,
        };

        // 检查是否有 -I 参数
        if len >= 3 {
            for item in args.iter() {
                if item == "-I" {
                    config.case_sensitive = false;
                    break;
                }
            }
        }

        Ok(config)
    }
}

// 逻辑主入口
// 返回 Result, 有错误向上抛出错误
// 没有错误, 就直接返回空元组
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
    // 读取文件可能会报错(比如没有权限之类的)
    // 不在这个位置直接处理, 直接向上传递错误
    let content = fs::read_to_string(&config.filename)?;

    // 判断是否需要忽略大小写(是否有 -I 参数)
    // 执行对应的搜索函数, 将结果赋值给 matched
    let matched = if config.case_sensitive {
        search(&config.query, &content)
    } else {
        search_case_insensitive(&config.query, &content)
    };

    for line in matched {
        println!("{}", line);
    }

    Ok(())
}

// 一行行读取内容然后遍历,保存遍历结果到 Vec 中
// 在这个函数中, query 并不需要保存到 Vec 中所有
// 无需注明生命周期
pub fn search<'a>(query: &str, content: &'a str) -> Vec<&'a str> {
    let mut matches: Vec<&str> = vec![];
    for line in content.lines() {
        if line.contains(query) {
            matches.push(line);
        }
    }
    matches
}

// 一行行读取内容然后遍历(忽略大小写), 保存遍历结果到 Vec 中
pub fn search_case_insensitive<'a>(query: &str, content: &'a str) -> Vec<&'a str> {
    let mut matches: Vec<&str> = vec![];

    // 将字符串切片转小写并返回一个新的字符串
    let search = query.to_lowercase();

    for line in content.lines() {
        if line.to_lowercase().contains(&search) {
            matches.push(line);
        }
    }

    matches
}

// 单元测试模块
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive_search() {
        let query = "hello";
        let content = "\
Alias quia laudantium.
Corporis rem assumenda.
hello In laborum sed.
Id consectetur eius fugiat
Labore rerum blanditiis
Hello, case_sensitive
";
        let expected = vec!["hello In laborum sed."];
        assert_eq!(expected, search(query, content));
    }

    #[test]
    fn case_insensitive_search() {
        let query = "hello";
        let content = "\
Alias quia laudantium.
Corporis rem assumenda.
hello In laborum sed.
Id consectetur eius fugiat
Labore rerum blanditiis
Hello, case_sensitive
";
        let expected = vec!["hello In laborum sed.", "Hello, case_sensitive"];
        assert_eq!(expected, search_case_insensitive(&query.to_lowercase(), content));
    }
}

使用测试

sh
cargo run notfound ./readme.txt
cargo run hello ./readme.txt
cargo run hello ./readme.txt -I

Released under the MIT License.