Skip to content

Commit

Permalink
Support more options for grep commands (#87)
Browse files Browse the repository at this point in the history
* Support addtional options
* Add the `colorize` feature to matching patterns
* Support regex search
  • Loading branch information
notJoon authored Oct 1, 2023
1 parent 6d36b22 commit 742e23c
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 39 deletions.
25 changes: 19 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ indoc = "2.0.3"
libloading = "0.8.0"
log = "0.4.18"
once_cell = "1.18.0"
regex = "1.9.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.104"
strsim = "0.10.0"
Expand Down
72 changes: 51 additions & 21 deletions src/commands/grep.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;
use std::{io, path::Path};

use regex::Regex;
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader};

Expand Down Expand Up @@ -139,39 +140,68 @@ impl GrepReport {
result
}

fn format_plain(&self) -> String {
fn format_plain(&self, hide_path: bool, list_of_files: bool, count: bool, patterns_to_search: Vec<String>, colorize: bool) -> String {
let mut result = String::new();

for dir in &self.directories {
let path = Path::new(&dir.name);

dir_path_pretty(path, &mut result);

for file in &dir.files {
let file_name = Path::new(&file.name);
let last_two = last_two(file_name);
result.push_str(&format!("{}\n", last_two[0]));

for item in &file.items {
result.push_str(&format!("{} {}", item.line, item.content.trim_start()));
let mut counter: usize = 0;

if !count {
for dir in &self.directories {
let path = Path::new(&dir.name);

if !hide_path {
dir_path_pretty(path, &mut result);
}

for file in &dir.files {
if !hide_path {
let file_name = Path::new(&file.name);
let last_two = last_two(file_name);
result.push_str(&format!("{}\n", last_two[0]));
}

if !list_of_files {
for item in &file.items {
if colorize {
// `(?i)` is for case insensitive search
let pattern = Regex::new(&format!(r"(?i){}", patterns_to_search.join(" "))).unwrap();
let text = &item.content;

let colored_text = pattern.replace_all(text, |caps: &regex::Captures| {
format!("\x1b[31m{}\x1b[0m", &caps[0])
});

result.push_str(&format!("{} {}", item.line, colored_text.trim_start()));
} else {
result.push_str(&format!("{} {}", item.line, item.content.trim_start()));
}
counter += 1;
}
result.push('\n');
} else {
counter += file.items.len();
}
}

result.push('\n');
}

result.push('\n');
result.push_str(&format!("\nTotal {} lines found\n", counter));
} else {
counter = self.directories
.iter()
.map(|dir| dir.files.iter().map(|file| file.items.len())
.sum::<usize>()).sum();
result = format!("Total {} lines found\n", counter);
}

result
}

pub fn report_formatting(&mut self, format: Option<String>) -> String {
#[allow(clippy::too_many_arguments)]
pub fn report_formatting(&mut self, format: Option<String>, hide_path: bool, list_of_files: bool, count: bool, patterns_to_search: Vec<String>, colorize: bool) -> String {
let default = "plain".to_string();
let format = format.unwrap_or(default);

match format.as_str() {
"json" => serde_json::to_string_pretty(self).unwrap(),
"plain" => self.format_plain(),
"plain" => self.format_plain(hide_path, list_of_files, count, patterns_to_search, colorize),
// "tree" => self.format_tree(4),
_ => {
let suggest = suggest_subcommand(&format).unwrap();
Expand Down
47 changes: 43 additions & 4 deletions src/commands/pattern_search.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
use crate::commands::boyer_moore::{BoyerMooreSearch, SearchIn};
use aho_corasick::AhoCorasick;
use regex::Regex;

#[derive(Debug, Clone)]
pub struct PatternTree;
pub struct PatternTree {
pub ignore_case: bool,
pub regex_flag: bool,
}

type PatternPosition = (bool, Vec<usize>);

#[allow(clippy::new_without_default)]
impl PatternTree {
pub fn new() -> Self {
PatternTree
PatternTree {
ignore_case: false,
regex_flag: false,
}
}

/// Call all search methods based on the given patterns
Expand All @@ -20,10 +27,27 @@ impl PatternTree {
/// Whereas, if the pattern is multiple, then call `aho_corasick_search` method.
/// AC is known as the fastest algorithm for multiple pattern search.
pub fn selective_search(&self, patterns: &Vec<String>, text: &str) -> PatternPosition {
if self.regex_flag {
return self.regex(text, &patterns[0]);
}

match patterns.len() {
0 => (false, vec![]),
1 => self.boyer_moore_search(text, &patterns[0]),
_ => self.aho_corasick_search(text, patterns),
1 => match self.ignore_case {
true => self.boyer_moore_search(&text.to_lowercase(), &patterns[0].to_lowercase()),
false => self.boyer_moore_search(text, &patterns[0]),
},
_ => {
if self.ignore_case {
let mut lower_patterns: Vec<String> = Vec::new();
patterns
.iter()
.for_each(|pattern| lower_patterns.push(pattern.to_lowercase()));
self.aho_corasick_search(&text.to_lowercase(), &lower_patterns)
} else {
self.aho_corasick_search(text, patterns)
}
}
}
}

Expand All @@ -44,4 +68,19 @@ impl PatternTree {

(!result.is_empty(), result)
}

pub fn regex(&self, text: &str, pattern: &String) -> PatternPosition {
let re = match self.ignore_case {
true => Regex::new(&format!(r"(?i){}", pattern)).unwrap(),
false => Regex::new(pattern).unwrap(),
};

let mut result: Vec<usize> = Vec::new();

for matched in re.find_iter(text) {
result.push(matched.start());
}

(!result.is_empty(), result)
}
}
103 changes: 95 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,52 @@ enum BalpanCommand {
Init,
#[clap(about = "Reset environment for Balpan and removes all TODO comments")]
Reset,
#[clap(about = "Search for TODO comments in the current repository")]
#[clap(
about = "Searches a particular pattern of characters, and displays all lines that contain that pattern"
)]
Grep {
#[clap(short, long, help = "Specific file to scan")]
#[clap(short = 'f', long, help = "Specific file to scan")]
file: Option<String>,
#[clap(short, long, help = "Specific pattern to search")]
#[clap(short = 'p', long, help = "Specific pattern to search")]
pattern: Option<String>,
#[clap(
long,
help = "Apply formatting to the output. Available options: json, tree, plain (default)"
)]
#[clap(
short = 'i',
long = "ignore",
help = "ignores the case(upper or lower) of the pattern."
)]
ignore_case: Option<Vec<String>>,
#[clap(
short = 'H',
help = "Display the matched lines, but do not display the filenames."
)]
hide_path: bool,
#[clap(
short = 'l',
help = "Display the names of files that contain matches, without displaying the matched lines."
)]
list_of_files: bool,
#[clap(
short = 'c',
help = "This prints only a count of the lines that match a pattern."
)]
count: bool,
#[clap(
short = 'T',
long = "time",
help = "Display the elapsed time during the execution of the command."
)]
show_elapsed_time: bool,
#[clap(short = 'o', help = "Colorize the matched pattern in the output.")]
colorize: bool,
#[clap(
short = 'E',
help = "Treats pattern as an extended regular expression (ERE)."
)]
extended_regex: bool,
format: Option<String>,
},
#[clap(about = "Generate a TODO comment for specific file")]
Expand Down Expand Up @@ -70,6 +106,13 @@ fn main() {
file,
pattern,
format,
ignore_case,
hide_path,
list_of_files,
count,
colorize,
extended_regex,
show_elapsed_time: elapsed,
} => {
let time = Instant::now();
let runtime = create_runtime();
Expand All @@ -79,10 +122,24 @@ fn main() {

runtime.block_on(async {
let mut report = GrepReport::new();
handle_grep(file, patterns, &mut report, format).await;
handle_grep(
file,
patterns,
&mut report,
format,
ignore_case,
hide_path,
list_of_files,
count,
colorize,
extended_regex,
)
.await;
});

println!("time: {:?}", time.elapsed());
if elapsed {
println!("time: {:?}", time.elapsed());
}
}
BalpanCommand::Analyze { pattern } => {
match pattern {
Expand Down Expand Up @@ -189,24 +246,54 @@ async fn handle_init() {
println!("init!");
}

#[allow(clippy::too_many_arguments)]
async fn handle_grep(
file: Option<String>,
pattern: Option<Vec<String>>,
report: &mut GrepReport,
format: Option<String>,
ignore_case: Option<Vec<String>>,
hide_path: bool,
list_of_files: bool,
count: bool,
colorize: bool,
extends_regex: bool,
) {
let mut pattern_tree = PatternTree::new();
let default_patterns = vec!["[TODO]".to_string(), "[DONE]".to_string()];
let patterns_to_search = pattern.unwrap_or(default_patterns);

let patterns_to_search: Vec<String>;

if extends_regex {
pattern_tree.ignore_case = true;
pattern_tree.regex_flag = true;
}

match ignore_case {
Some(ignore_patterns) => {
pattern_tree.ignore_case = true;
patterns_to_search = ignore_patterns;
}
None => {
patterns_to_search = pattern.unwrap_or(default_patterns);
}
}

match file {
Some(file_path) => {
scan_specific_file(file_path, report, &mut pattern_tree, &patterns_to_search).await
}
None => scan_project_directory(report, pattern_tree, patterns_to_search).await,
None => scan_project_directory(report, pattern_tree, patterns_to_search.clone()).await,
}

let formatting = report.report_formatting(format);
let formatting = report.report_formatting(
format,
hide_path,
list_of_files,
count,
patterns_to_search,
colorize,
);
println!("{}", formatting);
}

Expand Down

0 comments on commit 742e23c

Please sign in to comment.