grep_clone/lib.rs
1//! # Grep Clone
2//! A mini version of grep that supports searching a file with a substring.
3//!
4//! It's only implemented here for learning purposes.
5//!
6//! Project originally specified at: <https://doc.rust-lang.org/book/ch12-00-an-io-project.html>
7
8use std::env;
9use std::error::Error;
10use std::fs;
11
12/// # Struct that defines required/supported arguments
13/// The struct can be used to parse arguments from an iterator.
14pub struct Args {
15 /// Pattern used as a substring search.
16 pub query: String,
17 /// Path to the file that should be opened and used for searching.
18 pub file_path: String,
19 /// Whether the search should be case-sensitive or not.
20 pub ignore_case: bool,
21}
22
23impl Args {
24 /// # Builds arguments from an iterator
25 /// Expects argument in the order:
26 /// 0. executable_name
27 /// 1. query
28 /// 2. file_path
29 /// 3. ignore_case (optional)
30 ///
31 /// # Examples
32 ///
33 /// ```
34 /// let iter = vec![
35 /// String::from("grep_clone"),
36 /// String::from("Who"),
37 /// String::from("poem.txt"),
38 /// ].into_iter();
39 /// let args = grep_clone::Args::build(iter);
40 /// ```
41 pub fn build(mut args: impl Iterator<Item = String>) -> Result<Args, &'static str> {
42 args.next();
43
44 let query = match args.next() {
45 Some(val) => val,
46 None => return Err("Missing query"),
47 };
48
49 let file_path = match args.next() {
50 Some(val) => val,
51 None => return Err("Missing file path"),
52 };
53
54 let mut ignore_case: bool = false;
55 if env::var("IGNORE_CASE").is_ok() {
56 ignore_case = true;
57 }
58
59 let next = args.next().unwrap_or(String::from(""));
60 if next.contains("-i") || next.contains("--ignore-case") {
61 ignore_case = true;
62 }
63
64 Ok(Args {
65 query,
66 file_path,
67 ignore_case,
68 })
69 }
70}
71
72/// # Case insensitive search
73///
74/// Search ignoring case.
75///
76/// # Example
77///
78/// ```
79/// let contents = "
80/// who is
81/// this?
82/// ";
83/// let query = "who";
84/// assert_eq!(grep_clone::case_insensitive_search(query, contents), vec!["who is"]);
85/// ```
86pub fn case_insensitive_search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
87 let query = query.to_lowercase();
88
89 contents
90 .lines()
91 .filter(|line| line.to_lowercase().contains(&query))
92 .collect()
93}
94
95/// # Search
96///
97/// Search taking into account the character case.
98///
99/// # Example
100///
101/// ```
102/// let contents = "
103/// who is
104/// this?
105/// Who are you?
106/// ";
107/// let query = "who";
108/// assert_eq!(grep_clone::search(query, contents), vec!["who is"]);
109/// ```
110pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
111 contents
112 .lines()
113 .filter(|line| line.contains(query))
114 .collect()
115}
116
117
118/// # Run
119///
120/// Entrypoint used by binary executable.
121///
122/// # Example
123///
124/// ```
125/// let iter = vec![
126/// String::from("grep_clone"),
127/// String::from("Who"),
128/// String::from("poem.txt"),
129/// ].into_iter();
130/// let args = grep_clone::Args::build(iter).unwrap();
131/// grep_clone::run(args);
132/// ```
133pub fn run(args: Args) -> Result<(), Box<dyn Error>> {
134 let contents = fs::read_to_string(args.file_path)?;
135
136 let result: Vec<&str>;
137
138 if args.ignore_case {
139 result = case_insensitive_search(&args.query, &contents);
140 } else {
141 result = search(&args.query, &contents);
142 }
143 for line in result {
144 println!("{line}");
145 }
146 Ok(())
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn one_result() {
155 let query = "duct";
156 let contents = "\
157Rust:
158safe, fast, productive.
159Pick three.
160Duct tape.";
161
162 assert_eq!(vec!["safe, fast, productive."], search(query, contents));
163 }
164
165 #[test]
166 fn case_insensitive() {
167 let query: &str = "RuSt";
168 let contents = "\
169Rust:
170safe, fast, productive.
171Pick three.
172Trust me.";
173
174 assert_eq!(
175 vec!["Rust:", "Trust me."],
176 case_insensitive_search(query, contents)
177 );
178 }
179}