pandastd_mini_grep/
lib.rs

1//! # mini_grep
2//!
3//! ## Usage Example
4//! ```bash
5//! IGNORE_CASE=1 cargo run -- searchstring example-filename.txt
6//! ```
7
8use std::io::{Error, ErrorKind};
9use std::{env, fs};
10
11pub fn run(conf: Config) -> Result<(), Error> {
12    println!(
13        "ignore case: {}",
14        if conf.ignore_case { "true" } else { "false" }
15    );
16    println!("searching for \"{}\" in \"{}\"\n", conf.query, conf.path);
17    let contents = fs::read_to_string(conf.path)?;
18    let results = search(conf.ignore_case, &conf.query, &contents)?;
19    for line in results {
20        println!("{}", line);
21    }
22    Ok(())
23}
24
25/// Config 结构体, 用于存储命令行参数
26///
27/// # parameters
28/// 1. query: 查询字符串
29/// 2. path: 文件路径
30/// 3. ignore_case: 是否忽略大小写, 默认为 false, 可以通过环境变量 IGNORE_CASE 设置
31/// `ignore_case 为 1, true, TRUE, True 时为 true, 其他值为 false`
32pub struct Config {
33    query: String,
34    path: String,
35    ignore_case: bool,
36}
37
38impl Config {
39    pub fn new(args: &[String]) -> Result<Config, Error> {
40        if args.len() != 3 {
41            Err(Error::new(
42                ErrorKind::InvalidInput,
43                "need 2 arguments, [query] [path]",
44            ))?
45        }
46        // 跳过 args[0], 因为它是程序名
47        let query = &args[1];
48        let path = &args[2];
49        // env::var 返回一个 Result, 如果环境变量不存在, 则返回 Err
50        let ignore_case = match env::var("IGNORE_CASE") {
51            Ok(val) => val == "1" || val == "true" || val == "TRUE" || val == "True",
52            Err(_) => false,
53        };
54        // query 和 path 的所有权都在 parse_config 中, 所以需要 clone 才能返回
55        Ok(Config {
56            query: query.clone(),
57            path: path.clone(),
58            ignore_case,
59        })
60    }
61    // 优化后的 new 方法, 使用迭代器取代性能不好的 clone
62    pub fn iter_new(mut args: impl Iterator<Item = String>) -> Result<Config, Error> {
63        let mut index = 0;
64        let ignore_case = match env::var("IGNORE_CASE") {
65            Ok(val) => val == "1" || val == "true" || val == "TRUE" || val == "True",
66            Err(_) => false,
67        };
68        let mut cfg = Config {
69            query: String::new(),
70            path: String::new(),
71            ignore_case,
72        };
73        while let Some(arg) = args.next() {
74            match index {
75                1 => cfg.query = arg,
76                2 => cfg.path = arg,
77                _ => Err(Error::new(
78                    ErrorKind::InvalidInput,
79                    "need 2 arguments, [query] [path]",
80                ))?,
81            }
82            index += 1;
83        }
84        Ok(cfg)
85    }
86}
87
88pub fn search<'a>(
89    ignore_case: bool,
90    query: &str,
91    contents: &'a str,
92) -> Result<Vec<&'a str>, Error> {
93    let mut query = query.to_string();
94    if ignore_case {
95        query = query.to_lowercase();
96    }
97    let mut results = Vec::new();
98    for line in contents.lines() {
99        let mut search_line = line.to_string();
100        if ignore_case {
101            search_line = search_line.to_lowercase();
102        }
103        if search_line.contains(&query) {
104            results.push(line);
105        }
106    }
107    Ok(results)
108}
109
110// 使用迭代器改造 search, 用 filter 替代 for 循环, 用 collect 替代 Vec::new, 性能更好
111pub fn filter_search<'a>(
112    ignore_case: bool,
113    query: &str,
114    contents: &'a str,
115) -> Result<Vec<&'a str>, Error> {
116    let results = contents
117        .lines()
118        .filter(|line| {
119            let mut search_line = line.to_string();
120            let mut search_query = query.to_string();
121            if ignore_case {
122                search_line = search_line.to_lowercase();
123                search_query = search_query.to_lowercase();
124            }
125            search_line.contains(&search_query)
126        })
127        .collect();
128    Ok(results)
129}
130
131// 改造为 Config 结构体的 new 方法
132// fn parse_config(args: &[String]) -> Config {
133//     let query = &args[1];
134//     let path = &args[2];
135//     // query 和 path 的所有权都在 parse_config 中, 所以需要 clone 才能返回
136//     Config {
137//         query: query.clone(),
138//         path: path.clone(),
139//     }
140// }
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_new() {
148        let args = vec![
149            String::from("mini_grep"),
150            String::from("searchstring"),
151            String::from("example-filename.txt"),
152        ];
153        let conf = Config::new(&args).unwrap();
154        assert_eq!(conf.query, "searchstring");
155        assert_eq!(conf.path, "example-filename.txt");
156    }
157
158    #[test]
159    #[should_panic(expected = "need 2 arguments, [query] [path]")]
160    fn test_new_with_invalid_args() {
161        let args = vec![String::from("mini_grep"), String::from("searchstring")];
162        let _conf = Config::new(&args).unwrap();
163    }
164
165    #[test]
166    fn test_search() {
167        let query = "duct";
168        // \ 是 Rust 的换行符, 表示下一行也是字符串的一部分
169        let contents = "\
170Rust:
171safe, fast, productive.
172Pick three.";
173        let results = search(true, query, contents).unwrap();
174        assert_eq!(results, vec!["safe, fast, productive."]);
175    }
176
177    #[test]
178    #[should_panic(expected = "no results found for searchstring")]
179    fn test_search_with_invalid_query() {
180        let query = "searchstring";
181        let contents = "\
182Rust:
183safe, fast, productive.
184Pick three.";
185        // _ 开头的变量表示不使用该变量
186        // unwrap() 在接受 Err 时会 panic, 正好用 should_panic 来测试
187        let _results = search(true, query, contents).unwrap();
188    }
189}