rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs (ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r), fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like client of the process interface. search-test.nu: 42 cases match native tools.
159 lines
7.5 KiB
Rust
159 lines
7.5 KiB
Rust
//! GNU-style line search on ripgrep's regex engine.
|
|
use grep_matcher::Matcher;
|
|
use grep_regex::RegexMatcherBuilder;
|
|
use std::io::{self, Read, Write};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Default)]
|
|
struct Options {
|
|
patterns: Vec<String>, files: Vec<PathBuf>, recursive: bool,
|
|
number: bool, ignore_case: bool, invert: bool, list: bool, list_without: bool,
|
|
count: bool, fixed: bool, extended: bool, word: bool, whole: bool,
|
|
only: bool, quiet: bool, silent: bool, filename: Option<bool>,
|
|
before: usize, after: usize,
|
|
include: Vec<globset::GlobMatcher>, exclude: Vec<globset::GlobMatcher>,
|
|
exclude_dir: Vec<globset::GlobMatcher>,
|
|
}
|
|
fn parse() -> Result<Options, Box<dyn std::error::Error>> {
|
|
use lexopt::Arg::*;
|
|
let mut p = lexopt::Parser::from_env();
|
|
let mut o = Options::default();
|
|
while let Some(arg) = p.next()? {
|
|
match arg {
|
|
Short('r' | 'R') => o.recursive = true,
|
|
Short('n') => o.number = true,
|
|
Short('i') => o.ignore_case = true,
|
|
Short('v') => o.invert = true,
|
|
Short('l') => o.list = true,
|
|
Short('L') => o.list_without = true,
|
|
Short('c') => o.count = true,
|
|
Short('E') => o.extended = true,
|
|
Short('G') => o.extended = false,
|
|
Short('F') => o.fixed = true,
|
|
Short('w') => o.word = true,
|
|
Short('x') => o.whole = true,
|
|
Short('o') => o.only = true,
|
|
Short('q') => o.quiet = true,
|
|
Short('s') => o.silent = true,
|
|
Short('h') => o.filename = Some(false),
|
|
Short('H') => o.filename = Some(true),
|
|
Short('A') => o.after = p.value()?.to_string_lossy().parse()?,
|
|
Short('B') => o.before = p.value()?.to_string_lossy().parse()?,
|
|
Short('C') => { let n = p.value()?.to_string_lossy().parse()?; o.before = n; o.after = n; }
|
|
Short('e') => o.patterns.push(p.value()?.to_string_lossy().into_owned()),
|
|
Short('f') => {
|
|
let file = p.value()?;
|
|
let text = std::fs::read_to_string(file)?;
|
|
o.patterns.extend(text.lines().map(str::to_owned));
|
|
}
|
|
Long("include" | "exclude" | "exclude-dir") => {
|
|
let name = match arg { Long(name) => name.to_owned(), _ => unreachable!() };
|
|
let g = globset::Glob::new(&p.value()?.to_string_lossy())?.compile_matcher();
|
|
match name.as_str() { "include" => o.include.push(g), "exclude" => o.exclude.push(g), _ => o.exclude_dir.push(g) }
|
|
}
|
|
Value(v) => {
|
|
if o.patterns.is_empty() { o.patterns.push(v.to_string_lossy().into_owned()); }
|
|
else { o.files.push(v.into()); }
|
|
}
|
|
_ => return Err(arg.unexpected().into()),
|
|
}
|
|
}
|
|
if o.patterns.is_empty() { return Err("a pattern is required".into()); }
|
|
if o.files.is_empty() { o.files.push(if o.recursive { ".".into() } else { "-".into() }); }
|
|
Ok(o)
|
|
}
|
|
// GNU basic regex quotes operators that extended regex treats as operators.
|
|
fn basic(pattern: &str) -> String {
|
|
let mut out = String::new();
|
|
let mut chars = pattern.chars();
|
|
while let Some(c) = chars.next() {
|
|
if c == '\\' {
|
|
if let Some(n) = chars.next() {
|
|
if "()+?|{}".contains(n) { out.push(n); }
|
|
else { out.push('\\'); out.push(n); }
|
|
} else { out.push('\\'); }
|
|
} else {
|
|
if "()+?|{}".contains(c) { out.push('\\'); }
|
|
out.push(c);
|
|
}
|
|
}
|
|
out
|
|
}
|
|
fn allowed(path: &Path, o: &Options) -> bool {
|
|
let name = path.file_name().unwrap_or(path.as_os_str());
|
|
(o.include.is_empty() || o.include.iter().any(|g| g.is_match(name)))
|
|
&& !o.exclude.iter().any(|g| g.is_match(name))
|
|
}
|
|
fn run(o: Options) -> Result<i32, Box<dyn std::error::Error>> {
|
|
let patterns: Vec<_> = o.patterns.iter().map(|p| if o.fixed || o.extended { p.clone() } else { basic(p) }).collect();
|
|
let matcher = RegexMatcherBuilder::new().case_insensitive(o.ignore_case)
|
|
.fixed_strings(o.fixed).word(o.word).whole_line(o.whole).build_many(&patterns)?;
|
|
let names = o.filename.unwrap_or(o.files.len() > 1 || o.recursive);
|
|
let mut files = Vec::new();
|
|
let mut error = false;
|
|
for path in &o.files {
|
|
if o.recursive && path.is_dir() {
|
|
for entry in walkdir::WalkDir::new(path).follow_links(true).into_iter().filter_entry(|e| {
|
|
!e.file_type().is_dir() || !o.exclude_dir.iter().any(|g| g.is_match(e.file_name()))
|
|
}) {
|
|
match entry {
|
|
Ok(e) if e.file_type().is_file() && allowed(e.path(), &o) => files.push(e.into_path()),
|
|
Ok(_) => (),
|
|
Err(e) => { error = true; if !o.silent { eprintln!("grep: {e}"); } }
|
|
}
|
|
}
|
|
} else if allowed(path, &o) { files.push(path.clone()); }
|
|
}
|
|
let mut matched = false;
|
|
let mut out = io::BufWriter::new(io::stdout().lock());
|
|
for path in files {
|
|
let mut data = Vec::new();
|
|
let read = if path == Path::new("-") { io::stdin().read_to_end(&mut data) }
|
|
else { std::fs::File::open(&path).and_then(|mut f| f.read_to_end(&mut data)) };
|
|
if let Err(e) = read { error = true; if !o.silent { eprintln!("grep: {}: {e}", path.display()); } continue; }
|
|
let lines: Vec<_> = data.split_inclusive(|b| *b == b'\n').collect();
|
|
let hits: Vec<_> = lines.iter().map(|line| {
|
|
let line = line.strip_suffix(b"\n").unwrap_or(line);
|
|
matcher.is_match(line).map(|m| m != o.invert)
|
|
}).collect::<Result<_, _>>()?;
|
|
let count = hits.iter().filter(|v| **v).count();
|
|
matched |= count > 0;
|
|
if o.quiet && count > 0 { return Ok(0); }
|
|
let label = if path == Path::new("-") { "(standard input)".into() } else { path.to_string_lossy() };
|
|
if o.list || o.list_without {
|
|
if (o.list && count > 0) || (o.list_without && count == 0) { writeln!(out, "{label}")?; }
|
|
} else if o.count {
|
|
if names { write!(out, "{label}:")?; }
|
|
writeln!(out, "{count}")?;
|
|
} else if !o.quiet {
|
|
let context = o.before > 0 || o.after > 0;
|
|
let mut last = None;
|
|
for (i, line) in lines.iter().enumerate() {
|
|
let selected = hits[i];
|
|
let visible = selected || (context && (i.saturating_sub(o.after)..=(i + o.before).min(lines.len().saturating_sub(1))).any(|j| hits[j]));
|
|
if !visible { continue; }
|
|
if o.only && !selected { continue; }
|
|
if context && last.is_some_and(|last| i > last + 1) { writeln!(out, "--")?; }
|
|
last = Some(i);
|
|
let sep = if selected { ':' } else { '-' };
|
|
let mut emit = |bytes: &[u8]| -> io::Result<()> {
|
|
if names { write!(out, "{label}{sep}")?; }
|
|
if o.number { write!(out, "{}{sep}", i + 1)?; }
|
|
out.write_all(bytes)?;
|
|
if !bytes.ends_with(b"\n") { writeln!(out)?; }
|
|
Ok(())
|
|
};
|
|
if o.only {
|
|
if !o.invert { matcher.try_find_iter(line, |m| { if !m.is_empty() { emit(&line[m.start()..m.end()])?; } Ok::<_, io::Error>(true) })??; }
|
|
} else { emit(line)?; }
|
|
}
|
|
}
|
|
}
|
|
out.flush()?;
|
|
Ok(if error { 2 } else if matched { 0 } else { 1 })
|
|
}
|
|
fn main() {
|
|
let code = match parse().and_then(run) { Ok(code) => code, Err(e) => { eprintln!("grep: {e}"); 2 } };
|
|
std::process::exit(code);
|
|
}
|