1use colored::*;
2use std::error::Error;
3use std::{fs, vec};
4use std::collections::HashMap;
5
6pub struct Arguments {
7 pub query: String, pub file_name: String, pub help_option: bool, pub view_version: bool, pub case_ignore: bool, pub line_number: bool, pub query_count: bool, pub non_match: bool, pub line_count: bool, pub view_stats: bool, }
18
19pub struct LineInfo<'a> {
20 pub line_number: usize,
21 pub line_content: &'a str,
22}
23
24pub struct SearchResult<'a> {
25 pub line_info: Vec<LineInfo<'a>>,
26 pub count: usize,
27 pub line_count: usize,
28}
29
30impl Default for Arguments {
31 fn default() -> Self {
32 Arguments {
33 query: Default::default(),
34 file_name: Default::default(),
35 help_option: false,
36 view_version: false,
37 case_ignore: false,
38 line_number: false,
39 query_count: false,
40 non_match: false,
41 line_count: false,
42 view_stats: false,
43 }
44 }
45}
46
47impl Arguments {
48 pub fn new(args: &[String]) -> Result<Arguments, &'static str> {
50 let error_message = Err("Unknown command, run 'cargo new minigrep_help' to learn more\n");
53
54 if args.len() == 2 {
55 if args[1].eq("minigrep_help") {
57 return Ok(Arguments {
58 help_option: true,
59 ..Default::default()
60 });
61 }
62 } else if args.len() == 3 {
63 if args[1].eq("minigrep") {
64 if args[2].eq("-v") || args[2].eq("--version") {
67 return Ok(Arguments {
68 view_version: true,
69 ..Default::default()
70 });
71 }
72 }
73 else if args[2].eq("-S") || args[2].eq("--stats") {
74 return Ok(Arguments {
77 file_name: args[1].clone(),
78 view_stats: true,
79 ..Default::default()
80 });
81 }
82 else {
83 return Ok(Arguments {
86 query: args[1].clone(),
87 file_name: args[2].clone(),
88 ..Default::default()
89 });
90 }
91 } else if args.len() >= 4 {
92 let option_list = &args[3..];
97
98 let mut argument = Arguments {
99 query: args[1].clone(),
100 file_name: args[2].clone(),
101 ..Default::default()
102 };
103
104 if option_list.contains(&"-i".to_string())
105 || option_list.contains(&"--ignore-case".to_string())
106 {
107 argument.case_ignore = true;
108 }
109
110 if option_list.contains(&"-n".to_string())
111 || option_list.contains(&"--line-number".to_string())
112 {
113 argument.line_number = true;
114 }
115
116 if option_list.contains(&"-c".to_string())
117 || option_list.contains(&"--query-count".to_string())
118 {
119 argument.query_count = true;
120 }
121
122 if option_list.contains(&"-lc".to_string())
123 || option_list.contains(&"--line-count".to_string())
124 {
125 argument.line_count = true;
126 }
127
128 if option_list.contains(&"-I".to_string())
129 || option_list.contains(&"--invert-match".to_string())
130 {
131 argument.non_match = true;
132 }
133
134 return Ok(argument);
135 }
136
137 return error_message;
138 }
139}
140
141pub fn run(arguments: Arguments) -> Result<(), Box<dyn Error>> {
143 if arguments.help_option {
145 view_help_menu();
146 return Ok(());
147 }
148
149 if arguments.view_version {
151 view_version();
152 return Ok(());
153 }
154
155 if arguments.view_stats {
157 view_file_stats(arguments);
158 return Ok(());
159 }
160
161 let contents = fs::read_to_string(&arguments.file_name)?;
162 println!("\n{}", format!("{}:", &arguments.file_name).yellow());
163
164 let search_result = if arguments.case_ignore {
165 search_case_insensitive(&arguments.query, &contents)
166 }
167 else if arguments.non_match {
168 search_invert_match(&arguments.query, &contents)
169 }
170 else {
171 search(&arguments.query, &contents)
172 };
173
174 let lines = search_result.line_info;
175
176 if lines.len() == 0 {
177 println!("No results");
178 }
179
180 if arguments.line_number {
182 for line in lines {
183 println!("{}: {}", format!("{}", line.line_number).cyan(), line.line_content);
184 }
185 } else {
186 for line in lines {
187 println!("{}", line.line_content);
188 }
189 }
190
191 if arguments.query_count {
193 println!("{}", format!("\nCount: {}", search_result.count).green().bold());
194 }
195 else {
196 println!("");
197 }
198
199 if arguments.line_count {
201 println!("{}", format!("Line count: {}\n", search_result.line_count).green().bold());
202 }
203 Ok(())
204}
205
206pub fn search<'a>(query: &str, contents: &'a str) -> SearchResult<'a> {
207 let mut line_info: Vec<LineInfo> = vec![];
208 let mut count = 0;
209 let mut line_count = 0;
210
211 for (line_no, line) in contents.lines().into_iter().enumerate() {
213 if line.contains(query) {
215 line_info.push(LineInfo {
216 line_number: line_no,
217 line_content: line,
218 });
219 count += line.matches(query).count();
220 line_count += 1;
221 }
222 }
223
224 SearchResult { line_info, count, line_count}
225}
226
227pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> SearchResult<'a> {
228 let query = query.to_lowercase();
229 let mut line_info: Vec<LineInfo> = vec![];
230 let mut count = 0;
231 let mut line_count = 0;
232
233 for (line_no, line) in contents.lines().into_iter().enumerate() {
235 if line.to_lowercase().contains(&query) {
237 line_info.push(LineInfo {
238 line_number: line_no,
239 line_content: line,
240 });
241 count += line.to_ascii_lowercase().matches(&query).count();
242 line_count += 1;
243 }
244 }
245
246 SearchResult { line_info, count, line_count}
247}
248
249pub fn search_invert_match<'a>(query: &str, contents: &'a str) -> SearchResult<'a> {
250 let mut line_info: Vec<LineInfo> = vec![];
251 let mut line_count = 0;
252
253 for (line_no, line) in contents.lines().into_iter().enumerate() {
255 if !line.contains(query) {
257 line_info.push(LineInfo {
258 line_number: line_no,
259 line_content: line,
260 });
261 line_count += 1;
262 }
263 }
264
265 SearchResult { line_info, count: 0, line_count}
266}
267
268pub fn view_file_stats(arguments: Arguments) {
269 let contents = fs::read_to_string(&arguments.file_name).expect("Unknown File");
270 let contents = contents.to_lowercase();
271 let words_list: Vec<&str> = contents.split_whitespace().collect();
272 let mut word_count = HashMap::new();
273 let mut char_count = 0;
274 let mut line_count = 0;
275
276 for _line in contents.lines().into_iter() {
277 line_count += 1;
278 }
279
280 println!("\n{}\n", format!("{}", arguments.file_name).yellow().bold());
281
282 for word in words_list {
283 let count = word_count.entry(word).or_insert(0);
284 *count += 1;
285 }
286
287 println!("{}", format!("{0: <10} | {1: <10}", "Sequence", "Frequency").bold());
288 println!("{}", format!("----------------------").bold());
289
290 for (word, frequency) in word_count {
291 println!("{0: <10} {1} {2: <10}", word, format!("|").bold(), frequency);
292 char_count += word.len();
293 }
294
295 println!("\n{}", format!("{}: {}", "Character count", char_count).bold());
296 println!("{}\n", format!("{}: {}", "Line count", line_count).bold());
297}
298
299pub fn view_help_menu() {
300 println!("\nRust {} help menu\n", format!("minigrep").yellow().bold());
301
302 println!(
303 "{}: cargo run [QUERY] [FILE_NAME] [OPTION]",
304 format!("Usage").cyan().bold()
305 );
306 println!(
307 "{}: Search for QUERY i.e a word in the FILE_NAME provided",
308 format!("Description").cyan().bold()
309 );
310 println!(
311 "{}: 'cargo run hello hello_world.txt -i'\n",
312 format!("Example").cyan().bold()
313 );
314
315 println!("{}", format!("Pattern Selection:").yellow().bold());
318 println!(
319 "{}\tignore case distinctions\t\t'cargo run hello hello_world.txt -i'",
320 format!(" -i, --ignore-case").blue().bold()
321 );
322
323 println!("{}", format!("\nOutput Control:").yellow().bold());
326
327 println!(
329 "{}\tprint line numbers with output lines \t'cargo run hello hello_world.txt -n'",
330 format!(" -n, --line-number").blue().bold()
331 );
332
333 println!(
335 "{}\toutput total occurences of query \t'cargo run butter recipe.txt -c'",
336 format!(" -c, --query-count").blue().bold()
337 );
338
339 println!(
341 "{}\toutput total lines containing query \t'cargo run phone devices.txt -lc'",
342 format!(" -lc, --line-count").blue().bold()
343 );
344
345 println!(
347 "{}\toutput non-matching lines \t\t'cargo run puppy pets.txt -I'",
348 format!(" -I, --invert-match").blue().bold()
349 );
350
351 println!("{}", format!("\nMiscellaneous:").yellow().bold());
354
355 println!(
357 "{}\t\tdisplays file statistics\t\t'cargo run war_and_peace.txt -S'",
358 format!(" -S, --stats").blue().bold()
359 );
360
361
362 println!(
364 "{}\tdisplays version information\t\t'cargo run minigrep -v'",
365 format!(" -v, --version").blue().bold()
366 );
367
368 println!(
370 "{}\tdisplays help menu\t\t\t'cargo run minigrep_help'",
371 format!(" minigrep_help").blue().bold()
372 );
373}
374
375pub fn view_version() {
376 println!("\n{}", format!("Minigrep v1.0.0").yellow().bold());
377 println!("Made by Nithin for learning purposes only");
378 println!("https://github.com/nithinmanoj10/minigrep")
379}