Support Cargo scaffolding and human output, and find boolean expressions

This commit is contained in:
2026-11-11 21:57:37 +00:00
parent c7606b8ae9
commit 652ea7960f
7 changed files with 267 additions and 116 deletions
+153 -97
View File
@@ -1,11 +1,15 @@
//! Find world paths with short-circuit AND/OR predicates and directory pruning.
//! Find world paths with precedence-aware, short-circuit predicates and directory pruning.
use sausage_spawn::Command;
use std::{
io::{self, Write},
path::PathBuf,
};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
enum Predicate {
True,
Not(Box<Self>),
And(Box<Self>, Box<Self>),
Or(Box<Self>, Box<Self>),
Name(globset::GlobMatcher),
Path(globset::GlobMatcher),
Type(String),
@@ -15,75 +19,181 @@ enum Predicate {
Print(bool),
Exec(Vec<String>),
}
fn run() -> Result<i32, Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().skip(1).collect();
let mut i = 0;
let mut roots = Vec::new();
while i < args.len() && !args[i].starts_with('-') {
roots.push(PathBuf::from(&args[i]));
i += 1;
struct Parser<'a> {
args: &'a [String],
i: usize,
maxdepth: usize,
mindepth: usize,
explicit_action: bool,
}
impl Parser<'_> {
fn peek(&self) -> Option<&str> {
self.args.get(self.i).map(String::as_str)
}
if roots.is_empty() {
roots.push(".".into());
fn or(&mut self) -> Result<Predicate> {
let mut value = self.and()?;
while matches!(self.peek(), Some("-o" | "-or")) {
self.i += 1;
value = Predicate::Or(Box::new(value), Box::new(self.and()?));
}
Ok(value)
}
let mut groups = vec![Vec::new()];
let mut maxdepth = usize::MAX;
let mut mindepth = 0;
let mut explicit_action = false;
while i < args.len() {
let flag = &args[i];
i += 1;
let predicate = match flag.as_str() {
"-o" | "-or" => {
groups.push(Vec::new());
continue;
fn and(&mut self) -> Result<Predicate> {
let mut value = self.unary()?;
while !matches!(self.peek(), None | Some("-o" | "-or" | ")")) {
if matches!(self.peek(), Some("-a" | "-and")) {
self.i += 1;
}
value = Predicate::And(Box::new(value), Box::new(self.unary()?));
}
Ok(value)
}
fn unary(&mut self) -> Result<Predicate> {
let flag = self.args.get(self.i).ok_or("missing expression")?;
self.i += 1;
Ok(match flag.as_str() {
"!" | "-not" => Predicate::Not(Box::new(self.unary()?)),
"(" => {
let value = self.or()?;
if self.peek() != Some(")") {
return Err("missing closing parenthesis".into());
}
self.i += 1;
value
}
"-a" | "-and" => continue,
"-prune" => Predicate::Prune,
"-empty" => Predicate::Empty,
"-print" | "-print0" => {
explicit_action = true;
self.explicit_action = true;
Predicate::Print(flag == "-print0")
}
"-name" | "-path" | "-type" | "-maxdepth" | "-mindepth" | "-newer" => {
let value = args.get(i).ok_or("missing predicate argument")?;
i += 1;
let value = self.args.get(self.i).ok_or("missing predicate argument")?;
self.i += 1;
match flag.as_str() {
"-name" => Predicate::Name(globset::Glob::new(value)?.compile_matcher()),
"-path" => Predicate::Path(globset::Glob::new(value)?.compile_matcher()),
"-type" => Predicate::Type(value.clone()),
"-newer" => Predicate::Newer(std::fs::metadata(value)?.modified()?),
"-maxdepth" => {
maxdepth = value.parse()?;
continue;
self.maxdepth = value.parse()?;
Predicate::True
}
_ => {
mindepth = value.parse()?;
continue;
self.mindepth = value.parse()?;
Predicate::True
}
}
}
"-exec" => {
let start = i;
while i < args.len() && args[i] != ";" {
i += 1;
let start = self.i;
while self.i < self.args.len() && self.args[self.i] != ";" {
self.i += 1;
}
if i == args.len() || i == start {
if self.i == self.args.len() || self.i == start {
return Err("-exec requires a command and ;".into());
}
explicit_action = true;
let argv = args[start..i].to_vec();
i += 1;
self.explicit_action = true;
let argv = self.args[start..self.i].to_vec();
self.i += 1;
Predicate::Exec(argv)
}
_ => return Err(format!("unsupported predicate: {flag}").into()),
};
groups.last_mut().unwrap().push(predicate);
})
}
}
fn matches(
predicate: &Predicate,
entry: &walkdir::DirEntry,
prune: &mut bool,
status: &mut i32,
out: &mut impl Write,
) -> Result<bool> {
Ok(match predicate {
Predicate::True => true,
Predicate::Not(value) => !matches(value, entry, prune, status, out)?,
Predicate::And(left, right) => {
matches(left, entry, prune, status, out)? && matches(right, entry, prune, status, out)?
}
Predicate::Or(left, right) => {
matches(left, entry, prune, status, out)? || matches(right, entry, prune, status, out)?
}
Predicate::Name(g) => g.is_match(entry.file_name()),
Predicate::Path(g) => g.is_match(entry.path()),
Predicate::Type(t) => match t.as_str() {
"f" => entry.file_type().is_file(),
"d" => entry.file_type().is_dir(),
"l" => entry.file_type().is_symlink(),
_ => return Err("unsupported file type".into()),
},
Predicate::Newer(time) => entry.metadata()?.modified()? > *time,
Predicate::Empty => {
if entry.file_type().is_dir() {
std::fs::read_dir(entry.path())?.next().is_none()
} else {
entry.file_type().is_file() && entry.metadata()?.len() == 0
}
}
Predicate::Prune => {
*prune = true;
true
}
Predicate::Print(null) => {
out.write_all(entry.path().as_os_str().as_encoded_bytes())?;
out.write_all(if *null { b"\0" } else { b"\n" })?;
true
}
Predicate::Exec(argv) => {
out.flush()?;
let argv: Vec<_> = argv
.iter()
.map(|arg| arg.replace("{}", &entry.path().to_string_lossy()))
.collect();
match Command::new(&argv[0]).args(&argv[1..]).status() {
Ok(code) => code == 0,
Err(e) => {
eprintln!("find: {e}");
*status = 1;
false
}
}
}
})
}
fn run() -> Result<i32> {
let args: Vec<_> = std::env::args().skip(1).collect();
let mut parser = Parser {
args: &args,
i: 0,
maxdepth: usize::MAX,
mindepth: 0,
explicit_action: false,
};
let mut roots = Vec::new();
while parser
.peek()
.is_some_and(|s| !s.starts_with('-') && !matches!(s, "!" | "(" | ")"))
{
roots.push(PathBuf::from(&args[parser.i]));
parser.i += 1;
}
if roots.is_empty() {
roots.push(".".into());
}
let predicate = if parser.peek().is_none() {
Predicate::True
} else {
parser.or()?
};
if parser.peek().is_some() {
return Err("unexpected closing parenthesis".into());
}
let mut status = 0;
let mut out = io::BufWriter::new(io::stdout().lock());
for root in roots {
let mut walk = walkdir::WalkDir::new(root).max_depth(maxdepth).into_iter();
let mut walk = walkdir::WalkDir::new(root)
.max_depth(parser.maxdepth)
.into_iter();
while let Some(entry) = walk.next() {
let entry = match entry {
Ok(e) => e,
@@ -93,69 +203,15 @@ fn run() -> Result<i32, Box<dyn std::error::Error>> {
continue;
}
};
if entry.depth() < mindepth {
if entry.depth() < parser.mindepth {
continue;
}
let mut prune = false;
let mut matches = false;
for group in &groups {
let mut matched = true;
for predicate in group {
matched = match predicate {
Predicate::Name(g) => g.is_match(entry.file_name()),
Predicate::Path(g) => g.is_match(entry.path()),
Predicate::Type(t) => match t.as_str() {
"f" => entry.file_type().is_file(),
"d" => entry.file_type().is_dir(),
"l" => entry.file_type().is_symlink(),
_ => return Err("unsupported file type".into()),
},
Predicate::Newer(time) => entry.metadata()?.modified()? > *time,
Predicate::Empty => {
if entry.file_type().is_dir() {
std::fs::read_dir(entry.path())?.next().is_none()
} else {
entry.file_type().is_file() && entry.metadata()?.len() == 0
}
}
Predicate::Prune => {
prune = true;
true
}
Predicate::Print(null) => {
out.write_all(entry.path().as_os_str().as_encoded_bytes())?;
out.write_all(if *null { b"\0" } else { b"\n" })?;
true
}
Predicate::Exec(argv) => {
out.flush()?;
let argv: Vec<_> = argv
.iter()
.map(|arg| arg.replace("{}", &entry.path().to_string_lossy()))
.collect();
match Command::new(&argv[0]).args(&argv[1..]).status() {
Ok(code) => code == 0,
Err(e) => {
eprintln!("find: {e}");
status = 1;
false
}
}
}
};
if !matched {
break;
}
}
if matched {
matches = true;
break;
}
}
let matched = matches(&predicate, &entry, &mut prune, &mut status, &mut out)?;
if prune && entry.file_type().is_dir() {
walk.skip_current_dir();
}
if matches && !explicit_action {
if matched && !parser.explicit_action {
out.write_all(entry.path().as_os_str().as_encoded_bytes())?;
out.write_all(b"\n")?;
}