Support Cargo scaffolding and human output, and find boolean expressions
This commit is contained in:
+153
-97
@@ -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")?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user