use clap::{Arg, Command};
use color_eyre::Result;
use color_eyre::eyre::eyre;
use regex::Regex;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Read};
use crate::lfs;
fn add_match_mode_args(cmd: Command) -> Command {
cmd
.arg(Arg::new("ignore-case")
.short('i')
.long("ignore-case")
.help("Ignore case distinctions")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("invert-match")
.short('v')
.long("invert-match")
.help("Invert the sense of matching")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("word-regexp")
.short('w')
.long("word-regexp")
.help("Match only whole words")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("line-regexp")
.short('x')
.long("line-regexp")
.help("Match only whole lines")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("fixed-strings")
.short('F')
.long("fixed-strings")
.help("Interpret pattern as fixed strings")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("extended-regexp")
.short('E')
.long("extended-regexp")
.help("Interpret pattern as extended regular expressions")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("perl-regexp")
.short('P')
.long("perl-regexp")
.help("Interpret pattern as Perl regular expressions")
.action(clap::ArgAction::SetTrue))
}
fn add_output_args(cmd: Command) -> Command {
cmd
.arg(Arg::new("line-number")
.short('n')
.long("line-number")
.help("Print line numbers")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("quiet")
.short('q')
.long("quiet")
.help("Suppress all output")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("silent")
.long("silent")
.help("Suppress all output")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("count")
.short('c')
.long("count")
.help("Print only a count of matching lines per file")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("files_with_matches")
.short('l')
.long("files-with-matches")
.help("Print only names of files with matches")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("files_without_match")
.short('L')
.long("files-without-match")
.help("Print only names of files with no matches")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("no-messages")
.short('s')
.long("no-messages")
.help("Suppress error messages")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("only-matching")
.short('o')
.long("only-matching")
.help("Print only the matched parts of lines")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("text")
.short('a')
.long("text")
.help("Treat binary files as text")
.action(clap::ArgAction::SetTrue))
}
fn add_recursion_args(cmd: Command) -> Command {
cmd
.arg(Arg::new("recursive")
.short('r')
.long("recursive")
.help("Recursively search subdirectories")
.action(clap::ArgAction::SetTrue))
.arg(Arg::new("dereference-recursive")
.short('R')
.long("dereference-recursive")
.help("Recursively search subdirectories (follow symlinks)")
.action(clap::ArgAction::SetTrue))
}
fn add_pattern_source_args(cmd: Command) -> Command {
cmd
.arg(Arg::new("regexp")
.short('e')
.long("regexp")
.value_name("PATTERN")
.num_args(1)
.action(clap::ArgAction::Append)
.help("Use PATTERN as the pattern"))
.arg(Arg::new("file_patterns")
.short('f')
.long("file")
.value_name("FILE")
.num_args(1)
.help("Take patterns from FILE"))
.arg(Arg::new("pattern")
.help("Pattern to search for"))
}
fn add_file_args(cmd: Command) -> Command {
cmd
.arg(Arg::new("files")
.num_args(0..)
.help("Files to search (if none, read from stdin; - means stdin)"))
}
pub fn build_shared_args(cmd: Command) -> Command {
add_file_args(add_pattern_source_args(add_recursion_args(add_output_args(add_match_mode_args(cmd)))))
}
#[derive(Clone)]
pub enum MatchMode {
Basic,
Extended,
Fixed,
Perl,
}
#[derive(Clone)]
pub enum Matcher {
Regex(Regex),
Fixed(Vec<String>, bool),
}
pub struct GrepOptions {
pub patterns: Vec<String>,
pub files: Vec<String>,
pub ignore_case: bool,
pub line_number: bool,
pub invert_match: bool,
pub word_match: bool,
pub line_match: bool,
pub quiet: bool,
pub count: bool,
pub files_with_matches: bool,
pub files_without_match: bool,
pub no_messages: bool,
pub recursive: bool,
pub only_matching: bool,
#[allow(dead_code)]
pub text: bool,
pub match_mode: MatchMode,
}
pub fn parse_options(matches: &clap::ArgMatches) -> Result<GrepOptions> {
parse_shared_options(matches, MatchMode::Basic)
}
fn collect_patterns_from_regexp(matches: &clap::ArgMatches, patterns: &mut Vec<String>) -> bool {
let mut patterns_from_options = false;
for val in matches.get_many::<String>("regexp").into_iter().flatten() {
patterns_from_options = true;
for line in val.split('\n') {
let line = line.trim_end_matches('\r').to_string();
if !line.is_empty() {
patterns.push(line);
}
}
}
patterns_from_options
}
fn collect_patterns_from_file(matches: &clap::ArgMatches, patterns: &mut Vec<String>) -> Result<bool> {
let mut had_pattern_file = false;
if let Some(path) = matches.get_one::<String>("file_patterns") {
had_pattern_file = true;
let content = if path == "-" {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).map_err(|e| eyre!("grep: -: {}", e))?;
s
} else {
std::fs::read_to_string(path).map_err(|e| eyre!("grep: {}: {}", path, e))?
};
for line in content.lines() {
let line = line.trim_end_matches('\n').trim_end_matches('\r');
if !line.is_empty() {
patterns.push(line.to_string());
}
}
}
Ok(had_pattern_file)
}
fn collect_patterns_from_positional(matches: &clap::ArgMatches, patterns: &mut Vec<String>) {
if patterns.is_empty() {
if let Some(p) = matches.get_one::<String>("pattern") {
for line in p.split('\n') {
let mut line = line.trim_end_matches('\r').to_string();
if line.len() >= 2 && line.starts_with('"') && line.ends_with('"') {
line = line[1..line.len() - 1].to_string();
}
patterns.push(line);
}
}
}
}
fn extract_flags_from_matches(matches: &clap::ArgMatches, default_match_mode: MatchMode) -> (bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, bool, MatchMode) {
let ignore_case = matches.get_flag("ignore-case");
let line_number = matches.get_flag("line-number");
let invert_match = matches.get_flag("invert-match");
let word_match = matches.get_flag("word-regexp");
let line_match = matches.get_flag("line-regexp");
let quiet = matches.get_flag("quiet") || matches.get_flag("silent");
let count = matches.get_flag("count");
let files_with_matches = matches.get_flag("files_with_matches");
let files_without_match = matches.get_flag("files_without_match");
let no_messages = matches.get_flag("no-messages");
let only_matching = matches.get_flag("only-matching");
let recursive = matches.get_flag("recursive") || matches.get_flag("dereference-recursive");
let text = matches.get_flag("text");
let match_mode = if matches.get_flag("fixed-strings") {
MatchMode::Fixed
} else if matches.get_flag("extended-regexp") {
MatchMode::Extended
} else if matches.get_flag("perl-regexp") {
MatchMode::Perl
} else {
default_match_mode
};
(ignore_case, line_number, invert_match, word_match, line_match, quiet,
count, files_with_matches, files_without_match, no_messages, recursive,
only_matching, text, match_mode)
}
fn build_files_vector(matches: &clap::ArgMatches, patterns_from_options: bool, had_pattern_file: bool) -> Vec<String> {
let mut files: Vec<String> = matches.get_many::<String>("files")
.map(|vals| vals.cloned().collect())
.unwrap_or_default();
if patterns_from_options || had_pattern_file {
if let Some(p) = matches.get_one::<String>("pattern") {
files.insert(0, p.clone());
}
}
files
}
pub fn parse_shared_options(matches: &clap::ArgMatches, default_match_mode: MatchMode) -> Result<GrepOptions> {
let mut patterns: Vec<String> = Vec::new();
let patterns_from_options = collect_patterns_from_regexp(matches, &mut patterns);
let had_pattern_file = collect_patterns_from_file(matches, &mut patterns)?;
collect_patterns_from_positional(matches, &mut patterns);
if patterns.is_empty() && !had_pattern_file {
return Err(eyre!("grep: missing pattern"));
}
let files = build_files_vector(matches, patterns_from_options, had_pattern_file);
let (ignore_case, line_number, invert_match, word_match, line_match, quiet,
count, files_with_matches, files_without_match, no_messages, recursive,
only_matching, text, match_mode) = extract_flags_from_matches(matches, default_match_mode);
Ok(GrepOptions {
patterns,
files,
ignore_case,
line_number,
invert_match,
word_match,
line_match,
quiet,
count,
files_with_matches,
files_without_match,
no_messages,
recursive,
only_matching,
text,
match_mode,
})
}
pub fn command() -> Command {
build_shared_args(Command::new("grep")
.about("Search for patterns in files"))
}
fn display_name(path: &str) -> &str {
if path == "-" {
"(standard input)"
} else {
path
}
}
fn expand_path(path: &std::path::Path, follow_symlinks: bool) -> Vec<String> {
let mut result = Vec::new();
let metadata = match lfs::symlink_metadata(path) {
Ok(m) => m,
Err(_) => return result,
};
let is_symlink = metadata.file_type().is_symlink();
let target_is_dir = if is_symlink && follow_symlinks {
std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
} else {
metadata.is_dir()
};
if metadata.is_file() {
result.push(path.to_string_lossy().into_owned());
} else if target_is_dir {
if is_symlink && !follow_symlinks {
result.push(path.to_string_lossy().into_owned());
} else {
let dir_to_read = if is_symlink && follow_symlinks {
match std::fs::canonicalize(path) {
Ok(resolved) => resolved,
Err(_) => return result,
}
} else {
path.to_path_buf()
};
match std::fs::read_dir(&dir_to_read) {
Ok(entries) => {
for entry in entries {
if let Ok(entry) = entry {
let file_name = entry.file_name();
let subpath = path.join(&file_name);
let sub_metadata = match lfs::symlink_metadata(&subpath) {
Ok(m) => m,
Err(_) => continue,
};
if sub_metadata.is_file() {
result.push(subpath.to_string_lossy().into_owned());
} else if sub_metadata.is_dir() && !sub_metadata.file_type().is_symlink() {
result.extend(expand_path(&subpath, false));
}
}
}
}
Err(_) => {}
}
}
}
result
}
fn print_only_matches(name: &str, line_num: usize, matches: Vec<String>, multiple_files: bool, line_number: bool) {
for mat in matches {
if mat.is_empty() {
continue;
}
if multiple_files {
if line_number {
println!("{}:{}:{}", name, line_num + 1, mat);
} else {
println!("{}:{}", name, mat);
}
} else {
if line_number {
println!("{}:{}", line_num + 1, mat);
} else {
println!("{}", mat);
}
}
}
}
fn print_line_normal(name: &str, line_num: usize, line: &str, multiple_files: bool, line_number: bool) {
if multiple_files {
if line_number {
println!("{}:{}:{}", name, line_num + 1, line);
} else {
println!("{}:{}", name, line);
}
} else {
if line_number {
println!("{}:{}", line_num + 1, line);
} else {
println!("{}", line);
}
}
}
fn process_lines(
reader: Box<dyn BufRead>,
file_path: &str,
options: &GrepOptions,
matcher: &Matcher,
multiple_files: bool,
name_only: bool,
) -> (bool, bool, usize) {
let mut found_match = false;
let mut match_count = 0;
for (line_num, line_result) in reader.lines().enumerate() {
let line = match line_result {
Ok(l) => l,
Err(e) => {
if !options.no_messages {
eprintln!("grep: error reading {}: {}", file_path, e);
}
continue;
}
};
let matches = matcher.matches(&line, options);
let should_print = if options.invert_match { !matches } else { matches };
if should_print {
found_match = true;
match_count += 1;
if options.quiet {
return (true, false, match_count);
}
if name_only {
return (true, false, match_count);
}
if options.count {
continue;
}
let name = display_name(file_path);
if options.only_matching {
if line.is_empty() {
continue;
}
if let Some(matches) = matcher.get_matches(&line, options) {
if matches.iter().all(|m| m.is_empty()) {
continue;
}
print_only_matches(&name, line_num, matches, multiple_files, options.line_number);
}
} else {
print_line_normal(&name, line_num, &line, multiple_files, options.line_number);
}
}
}
(found_match, false, match_count)
}
fn search_file(file_path: &str, options: &GrepOptions, matcher: &Matcher, multiple_files: bool) -> Result<(bool, bool, usize)> {
let reader: Box<dyn BufRead> = if file_path == "-" {
Box::new(BufReader::new(io::stdin()))
} else {
if std::path::Path::new(file_path).is_dir() {
if !options.no_messages {
eprintln!("grep: {}: Is a directory", file_path);
}
return Ok((false, true, 0));
}
let file = match File::open(file_path) {
Ok(f) => f,
Err(e) => {
if !options.no_messages {
eprintln!("grep: {}: {}", file_path, e);
}
return Ok((false, true, 0));
}
};
Box::new(BufReader::new(file))
};
let name_only = options.files_with_matches || options.files_without_match;
Ok(process_lines(reader, file_path, options, matcher, multiple_files, name_only))
}
impl Matcher {
fn new(patterns: &[String], options: &GrepOptions) -> Result<Self> {
let never_match = Regex::new("[^\\s\\S]").unwrap();
if patterns.is_empty() || (patterns.len() == 1 && patterns[0].is_empty()) {
return Ok(Matcher::Regex(never_match));
}
match options.match_mode {
MatchMode::Fixed => {
Ok(Matcher::Fixed(patterns.to_vec(), options.ignore_case))
}
_ => {
let mut regex_pattern = String::new();
if options.ignore_case {
regex_pattern.push_str("(?i)");
}
let escaped: Vec<String> = match options.match_mode {
MatchMode::Extended | MatchMode::Perl => {
patterns.iter().map(|p| p.to_string()).collect()
}
MatchMode::Basic => {
patterns.iter()
.map(|p| p.replace('\\', "\\\\"))
.collect()
}
MatchMode::Fixed => unreachable!(),
};
let combined = escaped.join("|");
if options.word_match {
let word_combined: String = patterns.iter()
.map(|p| regex::escape(p))
.collect::<Vec<_>>()
.join("|");
regex_pattern.push_str(&format!(r"\b(?:{})\b", word_combined));
} else {
regex_pattern.push_str(&combined);
}
if options.line_match {
regex_pattern = format!(r"^(?:{})$", regex_pattern);
}
let regex = Regex::new(®ex_pattern)
.map_err(|e| eyre!("grep: invalid regex: {}", e))?;
Ok(Matcher::Regex(regex))
}
}
}
fn matches(&self, line: &str, options: &GrepOptions) -> bool {
match self {
Matcher::Regex(regex) => regex.is_match(line),
Matcher::Fixed(patterns, ignore_case) => {
let line_to_check = if *ignore_case {
line.to_lowercase()
} else {
line.to_string()
};
for pattern in patterns {
let pattern_to_check = if *ignore_case {
pattern.to_lowercase()
} else {
pattern.clone()
};
let matched = if options.word_match {
let word_regex = format!(r"\b{}\b", regex::escape(&pattern_to_check));
Regex::new(&word_regex).unwrap().is_match(&line_to_check)
} else if options.line_match {
line_to_check == pattern_to_check
} else {
line_to_check.contains(&pattern_to_check)
};
if matched {
return true;
}
}
false
}
}
}
fn get_matches(&self, line: &str, options: &GrepOptions) -> Option<Vec<String>> {
match self {
Matcher::Regex(regex) => {
let mut matches = Vec::new();
let mut search_start = 0;
while search_start <= line.len() {
let rest = &line[search_start..];
let mat = match regex.find(rest) {
Some(m) => m,
None => break,
};
let start = search_start + mat.start();
let end = search_start + mat.end();
if start == end {
if rest.is_empty() {
break;
}
search_start += 1; // advance past zero-length match to avoid infinite loop (do not push "")
} else {
matches.push(line[start..end].to_string());
search_start = end;
}
}
if matches.is_empty() { None } else { Some(matches) }
}
Matcher::Fixed(patterns, ignore_case) => {
let line_to_check = if *ignore_case {
line.to_lowercase()
} else {
line.to_string()
};
for pattern in patterns {
let pattern_to_check = if *ignore_case {
pattern.to_lowercase()
} else {
pattern.clone()
};
if options.word_match {
let word_regex = format!(r"\b{}\b", regex::escape(&pattern_to_check));
if let Ok(re) = Regex::new(&word_regex) {
let mut matches = Vec::new();
for mat in re.find_iter(&line_to_check) {
matches.push(line[mat.start()..mat.end()].to_string());
}
if !matches.is_empty() {
return Some(matches);
}
}
} else if options.line_match {
if line_to_check == pattern_to_check {
return Some(vec![line.to_string()]);
}
} else if line_to_check.contains(&pattern_to_check) {
let mut matches = Vec::new();
let mut start = 0;
while let Some(pos) = line_to_check[start..].find(&pattern_to_check) {
let actual_start = start + pos;
let actual_end = actual_start + pattern_to_check.len();
matches.push(line[actual_start..actual_end].to_string());
start = actual_end;
}
if !matches.is_empty() {
return Some(matches);
}
}
}
None
}
}
}
}
fn build_files_to_search(files: &[String], recursive: bool) -> (Vec<String>, bool) {
let mut files_to_search = Vec::new();
let mut has_dir_arg = false;
if files.is_empty() {
files_to_search.push("-".to_string());
return (files_to_search, has_dir_arg);
}
for file_path in files {
if recursive && file_path != "-" {
let path = std::path::Path::new(file_path);
let metadata = lfs::symlink_metadata(path).ok();
let is_symlink = metadata.as_ref().map(|m| m.file_type().is_symlink()).unwrap_or(false);
let follow_symlinks = is_symlink; // follow symlinks only if the argument itself is a symlink
let is_dir = if is_symlink && follow_symlinks {
// If symlink and we will follow it, check target type
std::fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
} else {
metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false)
};
if is_dir {
has_dir_arg = true;
files_to_search.extend(expand_path(path, follow_symlinks));
} else {
files_to_search.push(file_path.clone());
}
} else {
files_to_search.push(file_path.clone());
}
}
(files_to_search, has_dir_arg)
}
fn handle_files(matcher: &Matcher, options: &GrepOptions) -> Result<bool> {
let mut found_any_match = false;
let mut had_error = false;
let (files_to_search, has_dir_arg) = build_files_to_search(&options.files, options.recursive);
let multiple_files = files_to_search.len() > 1 || has_dir_arg;
for file_path in files_to_search {
let (found, err, count) = search_file(&file_path, options, matcher, multiple_files)?;
if err {
had_error = true;
}
let name = display_name(&file_path);
if options.files_with_matches && found {
println!("{}", name);
found_any_match = true;
}
if options.files_without_match && !found {
println!("{}", name);
found_any_match = true;
}
if options.count {
if multiple_files {
println!("{}:{}", name, count);
} else {
println!("{}", count);
}
if count > 0 {
found_any_match = true;
}
}
if found && !options.files_with_matches && !options.files_without_match && !options.count {
found_any_match = true;
if options.quiet {
std::process::exit(0);
}
}
}
// Exit code: 2 if any error (unless -q and we found a match), 1 if no match, 0 if match
if had_error && !(options.quiet && found_any_match) {
std::process::exit(2);
}
Ok(found_any_match)
}
pub fn run(options: GrepOptions) -> Result<()> {
let matcher = Matcher::new(&options.patterns, &options)?;
let found_any_match = handle_files(&matcher, &options)?;
if !found_any_match {
std::process::exit(1);
}
Ok(())
}