Files
sausage/userland/grep/src/main.rs
T
surma b9e3e3677d userland: search and exec tools, spawn library (T14)
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.
2026-11-06 17:21:51 +00:00

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);
}