use std::fs;
use std::io::{self, BufWriter, Read, Write};
use std::mem;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::{Context, Result, bail};
use clap::{ArgGroup, Args};
use grep::printer::{
JSON, JSONBuilder, Standard, StandardBuilder, Summary, SummaryBuilder, SummaryKind,
};
use grep::regex::{RegexMatcher, RegexMatcherBuilder};
use grep::searcher::{BinaryDetection, Searcher, SearcherBuilder, Sink, SinkMatch};
use ignore::types::TypesBuilder;
use termcolor::NoColor;
use veloci::FormatHint;
use crate::util::{
CONFIG_ENV, ConfigArg, DetectorArg, Fatal, RulesCache, WalkOptions, display_path,
for_each_ordered, is_broken_pipe, is_file, walk_builder,
};
#[derive(Debug, Args)]
#[command(group(ArgGroup::new("mode").multiple(false)))]
pub struct GrepArgs {
#[arg(value_name = "PATTERN|PATH")]
positional: Vec<String>,
#[arg(
short = 'e',
long = "regexp",
value_name = "PATTERN",
allow_hyphen_values = true
)]
regexp: Vec<String>,
#[arg(short = 'F', long)]
fixed_strings: bool,
#[arg(short = 'i', long)]
ignore_case: bool,
#[arg(short = 'S', long)]
smart_case: bool,
#[arg(short = 'w', long)]
word_regexp: bool,
#[arg(short = 'x', long)]
line_regexp: bool,
#[arg(short = 'v', long)]
invert_match: bool,
#[arg(short = 'U', long)]
multiline: bool,
#[arg(long, requires = "multiline")]
multiline_dotall: bool,
#[arg(short = 'g', long, value_name = "GLOB")]
glob: Vec<String>,
#[arg(short = 't', long = "type", value_name = "TYPE")]
file_type: Vec<String>,
#[arg(short = 'T', long = "type-not", value_name = "TYPE")]
type_not: Vec<String>,
#[arg(long)]
hidden: bool,
#[arg(long)]
no_ignore: bool,
#[arg(short = 'L', long)]
follow: bool,
#[arg(short = 'd', long, value_name = "NUM")]
max_depth: Option<usize>,
#[arg(short = 'r', long, short_alias = 'R', hide = true)]
recursive: bool,
#[arg(short = 'n', long, overrides_with = "no_line_number")]
line_number: bool,
#[arg(short = 'N', long)]
no_line_number: bool,
#[arg(short = 'H', long, overrides_with = "no_filename")]
with_filename: bool,
#[arg(short = 'I', long)]
no_filename: bool,
#[arg(short = 'l', long, group = "mode")]
files_with_matches: bool,
#[arg(long, group = "mode")]
files_without_match: bool,
#[arg(short = 'c', long, group = "mode")]
count: bool,
#[arg(short = 'q', long, group = "mode")]
quiet: bool,
#[arg(long, group = "mode")]
json: bool,
#[arg(short = 'o', long)]
only_matching: bool,
#[arg(short = 'A', long, value_name = "NUM")]
after_context: Option<usize>,
#[arg(short = 'B', long, value_name = "NUM")]
before_context: Option<usize>,
#[arg(short = 'C', long, value_name = "NUM")]
context: Option<usize>,
#[arg(short = 'm', long, value_name = "NUM")]
max_count: Option<u64>,
#[arg(long, value_name = "FILE", env = CONFIG_ENV)]
config: Option<PathBuf>,
#[command(flatten)]
enable: DetectorArg,
}
enum Printer {
Standard(Standard<NoColor<Vec<u8>>>),
Summary(Summary<NoColor<Vec<u8>>>),
Json(JSON<Vec<u8>>),
}
impl Printer {
fn search(
&mut self,
searcher: &mut Searcher,
matcher: &RegexMatcher,
path: &Path,
data: &[u8],
) -> io::Result<bool> {
fn run<S: Sink<Error = io::Error>>(
searcher: &mut Searcher,
matcher: &RegexMatcher,
data: &[u8],
mut sink: S,
has_match: impl Fn(&S) -> bool,
) -> io::Result<bool> {
searcher.search_slice(matcher, data, &mut sink)?;
Ok(has_match(&sink))
}
match self {
Printer::Standard(p) => run(
searcher,
matcher,
data,
p.sink_with_path(matcher, path),
|s| s.has_match(),
),
Printer::Summary(p) => run(
searcher,
matcher,
data,
p.sink_with_path(matcher, path),
|s| s.has_match(),
),
Printer::Json(p) => run(
searcher,
matcher,
data,
p.sink_with_path(matcher, path),
|s| s.has_match(),
),
}
}
fn take(&mut self) -> Vec<u8> {
match self {
Printer::Standard(p) => mem::take(p.get_mut().get_mut()),
Printer::Summary(p) => mem::take(p.get_mut().get_mut()),
Printer::Json(p) => mem::take(p.get_mut()),
}
}
}
struct Probe(bool);
impl Sink for Probe {
type Error = io::Error;
fn matched(&mut self, _: &Searcher, _: &SinkMatch<'_>) -> io::Result<bool> {
self.0 = true;
Ok(false)
}
}
enum Source {
Stdin,
Entry(Result<ignore::DirEntry, ignore::Error>),
}
#[derive(Default)]
struct Outcome {
output: Vec<u8>,
messages: Vec<String>,
hit: bool,
failed: bool,
fatal: bool,
}
struct Shared<'a> {
args: &'a GrepArgs,
matcher: RegexMatcher,
rules: RulesCache,
show_path: bool,
implicit_root: bool,
}
struct Worker<'a> {
shared: &'a Shared<'a>,
probe: Searcher,
searcher: Searcher,
printer: Printer,
}
impl<'a> Worker<'a> {
fn new(shared: &'a Shared<'a>) -> Self {
let args = shared.args;
Worker {
shared,
probe: SearcherBuilder::new()
.binary_detection(BinaryDetection::quit(b'\x00'))
.invert_match(args.invert_match)
.multi_line(args.multiline)
.line_number(false)
.build(),
searcher: searcher(args),
printer: printer(args, shared.show_path),
}
}
fn search(&mut self, source: Source) -> Outcome {
let mut outcome = Outcome::default();
let result = match source {
Source::Stdin => {
let mut data = Vec::new();
io::stdin()
.read_to_end(&mut data)
.context("reading standard input")
.and_then(|_| {
self.search_data(Path::new("-"), Path::new("<stdin>"), &data, &mut outcome)
})
}
Source::Entry(Err(err)) => Err(err.into()),
Source::Entry(Ok(entry)) if is_file(&entry) => {
self.search_file(entry.path(), &mut outcome)
}
Source::Entry(Ok(_)) => Ok(()),
};
if let Err(err) = result {
outcome.messages.push(format!("error: {err:#}"));
outcome.failed = true;
outcome.fatal = err.is::<Fatal>();
}
outcome.output = self.printer.take();
outcome
}
fn search_file(&mut self, path: &Path, outcome: &mut Outcome) -> Result<()> {
let display = display_path(path, self.shared.implicit_root);
let data = fs::read(path).with_context(|| format!("reading {}", display.display()))?;
self.search_data(path, display, &data, outcome)
}
fn search_data(
&mut self,
path: &Path,
display: &Path,
data: &[u8],
outcome: &mut Outcome,
) -> Result<()> {
let matcher = &self.shared.matcher;
let mut probe = Probe(false);
self.probe
.search_slice(matcher, data, &mut probe)
.with_context(|| format!("searching {}", display.display()))?;
if !probe.0 {
outcome.hit = self
.printer
.search(&mut self.searcher, matcher, display, b"")?;
return Ok(());
}
let stdin = path == Path::new("-");
let (rules, hint, file) = if stdin {
(
self.shared.rules.for_directory(None)?,
FormatHint::Auto,
None,
)
} else {
(
self.shared.rules.for_file(path)?,
FormatHint::Path(path),
Some(path),
)
};
let redaction = rules
.redactor_for(file)
.redact(data, hint)
.with_context(|| format!("redacting {}", display.display()))?;
for warning in redaction.warnings() {
outcome
.messages
.push(format!("warning: {}: {warning}", display.display()));
}
let redacted = redaction
.render(rules.allow_for(file))
.with_context(|| format!("redacting {}", display.display()))?;
outcome.hit = self
.printer
.search(&mut self.searcher, matcher, display, &redacted)?;
Ok(())
}
}
pub fn grep(args: GrepArgs) -> Result<ExitCode> {
match run(&args) {
Err(err) if is_broken_pipe(&err) => Ok(ExitCode::SUCCESS),
result => result,
}
}
fn run(args: &GrepArgs) -> Result<ExitCode> {
let (patterns, paths, implicit_root) = split_positional(args)?;
let shared = Shared {
args,
matcher: matcher(args, &patterns)?,
rules: RulesCache::new(ConfigArg {
config: args.config.clone(),
enable: args.enable.clone(),
}),
show_path: !args.no_filename
&& (args.with_filename || paths.len() > 1 || paths.iter().any(|p| p.is_dir())),
implicit_root,
};
let mut sources: Vec<Box<dyn Iterator<Item = Source> + Send>> = Vec::new();
for path in &paths {
if path == Path::new("-") {
sources.push(Box::new(std::iter::once(Source::Stdin)));
} else {
sources.push(Box::new(walker(args, path)?.map(Source::Entry)));
}
}
let context = args.context.or(args.before_context).or(args.after_context);
let separate_files = matches!(printer(args, false), Printer::Standard(_))
&& context.is_some_and(|lines| lines > 0);
let mut failed = false;
let mut found = false;
let mut printed = false;
let mut stdout = BufWriter::new(io::stdout().lock());
let stop = AtomicBool::new(false);
let unreported = for_each_ordered(
sources.into_iter().flatten(),
&stop,
|| Worker::new(&shared),
|worker, source| {
let outcome = worker.search(source);
if outcome.fatal {
stop.store(true, Ordering::Relaxed);
}
outcome
},
|mut outcome| {
if !outcome.messages.is_empty() {
stdout.flush().context("writing standard output")?;
for message in &outcome.messages {
eprintln!("{message}");
}
}
failed |= outcome.failed;
found |= outcome.hit;
if outcome.fatal || (args.quiet && found) {
return Ok(false);
}
if outcome.output.is_empty() {
return Ok(true);
}
if separate_files && printed {
outcome.output.splice(0..0, *b"--\n");
}
printed = true;
stdout
.write_all(&outcome.output)
.context("writing standard output")?;
Ok(true)
},
)?;
if let Some(outcome) = unreported.iter().find(|outcome| outcome.fatal) {
stdout.flush().context("writing standard output")?;
for message in &outcome.messages {
eprintln!("{message}");
}
failed = true;
}
stdout.flush().context("writing standard output")?;
Ok(if args.quiet && found {
ExitCode::SUCCESS
} else if failed {
ExitCode::from(2)
} else if found {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
})
}
fn split_positional(args: &GrepArgs) -> Result<(Vec<String>, Vec<PathBuf>, bool)> {
let mut positional = args.positional.iter();
let mut patterns = Vec::new();
if args.regexp.is_empty() {
let Some(pattern) = positional.next() else {
bail!("no pattern given");
};
patterns.push(pattern.clone());
}
patterns.extend(args.regexp.iter().cloned());
let paths: Vec<PathBuf> = positional.map(PathBuf::from).collect();
if paths.is_empty() {
return Ok((patterns, vec![PathBuf::from(".")], true));
}
Ok((patterns, paths, false))
}
fn matcher(args: &GrepArgs, patterns: &[String]) -> Result<RegexMatcher> {
let mut builder = RegexMatcherBuilder::new();
builder
.case_insensitive(args.ignore_case)
.case_smart(args.smart_case && !args.ignore_case)
.word(args.word_regexp)
.whole_line(args.line_regexp)
.fixed_strings(args.fixed_strings)
.multi_line(true)
.dot_matches_new_line(args.multiline_dotall);
if !args.multiline {
builder.line_terminator(Some(b'\n'));
}
builder.build_many(patterns).context("parsing the pattern")
}
fn searcher(args: &GrepArgs) -> Searcher {
let before = args.before_context.or(args.context).unwrap_or(0);
let after = args.after_context.or(args.context).unwrap_or(0);
SearcherBuilder::new()
.binary_detection(BinaryDetection::quit(b'\x00'))
.invert_match(args.invert_match)
.multi_line(args.multiline)
.line_number(!args.no_line_number)
.before_context(before)
.after_context(after)
.max_matches(args.max_count)
.build()
}
fn printer(args: &GrepArgs, show_path: bool) -> Printer {
let out = Vec::new();
let summary = if args.files_with_matches {
Some(SummaryKind::PathWithMatch)
} else if args.files_without_match {
Some(SummaryKind::PathWithoutMatch)
} else if args.count {
Some(SummaryKind::Count)
} else if args.quiet {
Some(SummaryKind::QuietWithMatch)
} else {
None
};
if let Some(kind) = summary {
Printer::Summary(
SummaryBuilder::new()
.kind(kind)
.path(show_path)
.build_no_color(out),
)
} else if args.json {
Printer::Json(JSONBuilder::new().build(out))
} else {
Printer::Standard(
StandardBuilder::new()
.path(show_path)
.only_matching(args.only_matching)
.build_no_color(out),
)
}
}
fn walker(args: &GrepArgs, root: &Path) -> Result<ignore::Walk> {
let mut builder = walk_builder(
root,
&WalkOptions {
globs: &args.glob,
hidden: args.hidden,
ignored: args.no_ignore,
follow: args.follow,
max_depth: args.max_depth,
skipped_dirs: &[],
},
)?;
let mut types = TypesBuilder::new();
types.add_defaults();
for name in &args.file_type {
types.select(name);
}
for name in &args.type_not {
types.negate(name);
}
builder.types(types.build().context("parsing --type")?);
Ok(builder.build())
}