实现一个 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