» Make grep CLI App in Rust » 2. Development » 2.3 Add Functionalities

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, greps 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 {