Add Functionalities
You need to support these functionalities as shown in the project objectives section:
-c, --count
-i, --ignore-case
-n, --line-number
-r, --recursive
-v, --invert-match
main.rs:
use clap::{App, Arg};
use lr_grustep::{grep, grep_count, grep_recursive, GrepOptions, MatchResult};
fn main() {
// Define command-line arguments using clap
let matches = App::new("grustep")
.version("0.1.0")
.author("literank")
.about("A grep-like utility in Rust")
.arg(Arg::with_name("pattern").required(true).index(1).help("The pattern to search for"))
.arg(Arg::with_name("file_path").required(true).index(2).help("The file to search in"))
.arg(Arg::with_name("count").short("c").long("count").help("Only a count of selected lines is written to standard output"))
.arg(Arg::with_name("ignore-case").short("i").long("ignore-case").help("Perform case-insensitive matching"))
.arg(Arg::with_name("line-number").short("n").long("line-number").help("Each output line is preceded by its relative line number in the file, starting at line 1"))
.arg(Arg::with_name("recursive").short("r").long("recursive").help("Recursively search subdirectories listed"))
.arg(Arg::with_name("invert-match").short("v").long("invert-match").help("Selected lines are those not matching any of the specified patterns"))
.get_matches();
// Extract command-line arguments
let pattern = matches.value_of("pattern").unwrap();
let file_path = matches.value_of("file_path").unwrap();
let options = GrepOptions {
ignore_case: matches.is_present("ignore-case"),
invert_match: matches.is_present("invert-match"),
};
let result = if matches.is_present("recursive") {
grep_recursive(pattern, file_path.as_ref(), &options)
} else {
grep(pattern, file_path.as_ref(), &options)
};
match result {
Ok(result) => {
if matches.is_present("count") {
println!("{}", grep_count(&result));
} else {
print_result(&result, matches.is_present("line-number"))
}
}
Err(err) => {
eprintln!("Error: {}", err);
}
}
}
fn print_result(result: &MatchResult, show_line_number: bool) {
let mut current_file = "";
let file_count = result.len();
for (file_path, items) in result {
for item in items {
if file_count > 1 && file_path != current_file {
current_file = &file_path;
println!("\n{}:", current_file);
}
if show_line_number {
println!("{}: {}", item.line_number, item.line);
} else {
println!("{}", item.line);
}
}
}
}
Crate clap
adds all the optional arguments into the app. Function print_result
parses the MatchResult
and prints the file_path
and line_number
if needed.
lib.rs:
use std::fs;
use std::io::{self, BufRead};
use std::path::Path;
use regex::Regex;
use walkdir::WalkDir;
// MatchItem represents a match in grep searching
pub struct MatchItem {
pub line_number: usize,
pub line: String,
}
// MatchResult represents all matches of all files of a grep search
pub type MatchResult = std::collections::HashMap<String, Vec<MatchItem>>;
pub struct GrepOptions {
pub ignore_case: bool,
pub invert_match: bool,
}
pub fn grep(
pattern: &str,
file_path: &Path,
options: &GrepOptions,
) -> Result<MatchResult, io::Error> {
let real_pattern = if options.ignore_case {
format!("(?i){}", pattern)
} else {
pattern.to_string()
};
let pattern_regex = regex::Regex::new(&real_pattern)
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("Regex error: {}", err)))?;
let lines = read_file_lines(file_path)?;
let matching_lines = if options.invert_match {
filter_lines(&pattern_regex, &lines, false)
} else {
filter_lines(&pattern_regex, &lines, true)
};
let mut result = MatchResult::new();
result.insert(file_path.to_string_lossy().to_string(), matching_lines);
Ok(result)
}
pub fn grep_count(result: &MatchResult) -> usize {
result.values().map(|v| v.len()).sum()
}
pub fn grep_recursive(
pattern: &str,
directory_path: &Path,
options: &GrepOptions,
) -> Result<MatchResult, io::Error> {
let mut results = MatchResult::new();
for entry in WalkDir::new(directory_path) {
let entry = entry?;
if entry.file_type().is_file() {
let file_path = entry.path();
let result = grep(pattern, &file_path, options)?;
results.extend(result);
}
}
Ok(results)
}
fn read_file_lines(file_path: &Path) -> Result<Vec<String>, io::Error> {
let file = fs::File::open(file_path)?;
let reader = io::BufReader::new(file);
Ok(reader.lines().filter_map(|line| line.ok()).collect())
}
fn filter_lines(pattern: &Regex, lines: &[String], flag: bool) -> Vec<MatchItem> {
lines
.iter()
.enumerate()
.filter_map(|(line_number, line)| {
if flag == pattern.is_match(line) {
Some(MatchItem {
line_number: line_number + 1,
line: line.trim().to_string(),
})
} else {
None
}
})
.collect()
}
Function grep
adds logic for "case-insensitive match" and "invert match."
Function grep_recursive
lists all files recursively, grep
s them, and puts the results into the MatchResult
.
Cargo.toml:
...
[dependencies]
regex = "1.5"
clap = "2.33"
walkdir = "2.3"
Add dependency crates clap
and walkdir
.
Run the normal way:
cargo run result src/main.rs
Result lines:
let result = if matches.is_present("recursive") {
match result {
Ok(result) => {
println!("{}", grep_count(&result));
print_result(&result, matches.is_present("line-number"))
fn print_result(result: &MatchResult, show_line_number: bool) {
let file_count = result.len();
for (file_path, items) in result {
Do the count:
# Double dash (--) to separate Cargo options from your program's options
cargo run -- -c result src/main.rs
Result number is: 8.
Show the line numbers:
cargo run -- -n result src/main.rs
Result lines:
27: let result = if matches.is_present("recursive") {
33: match result {
34: Ok(result) => {
36: println!("{}", grep_count(&result));
38: print_result(&result, matches.is_present("line-number"))
47: fn print_result(result: &MatchResult, show_line_number: bool) {
49: let file_count = result.len();
51: for (file_path, items) in result {
Use regex pattern
cargo run -- -n "\br[a-z]{4}t" src/main.rs
Result lines:
27: let result = if matches.is_present("recursive") {
33: match result {
34: Ok(result) => {
36: println!("{}", grep_count(&result));
38: print_result(&result, matches.is_present("line-number"))
47: fn print_result(result: &MatchResult, show_line_number: bool) {
49: let file_count = result.len();
51: for (file_path, items) in result {
Do the invert match:
cargo run -- -vn result src/main.rs
Result is something like this:
1: use clap::{App, Arg};
2: use lr_grustep::{grep, grep_count, grep_recursive, GrepOptions, MatchResult};
3:
4: fn main() {
5: // Define command-line arguments using clap
6: let matches = App::new("grustep")
7: .version("0.1.0")
...
19: // Extract command-line arguments
20: let pattern = matches.value_of("pattern").unwrap();
21: let file_path = matches.value_of("file_path").unwrap();
22: let options = GrepOptions {
23: ignore_case: matches.is_present("ignore-case"),
24: invert_match: matches.is_present("invert-match"),
25: };
...
57: if show_line_number {
58: println!("{}: {}", item.line_number, item.line);
59: } else {
60: println!("{}", item.line);
61: }
62: }
...
Do the case insensitive match:
cargo run -- -i only src/main.rs
Result is:
.arg(Arg::with_name("count").short("c").long("count").help("Only a count of selected lines is written to standard output"))
Do the big recursive match:
cargo run -- -r count ./src
Result lines should be like:
./src/lib.rs:
pub fn grep_count(result: &MatchResult) -> usize {
./src/main.rs:
use lr_grustep::{grep, grep_count, grep_recursive, GrepOptions, MatchResult};
.arg(Arg::with_name("count").short("c").long("count").help("Only a count of selected lines is written to standard output"))
if matches.is_present("count") {
println!("{}", grep_count(&result));
let file_count = result.len();
if file_count > 1 && file_path != current_file {