176 lines
6.4 KiB
Rust
176 lines
6.4 KiB
Rust
//! Find world paths with short-circuit AND/OR predicates and directory pruning.
|
|
use sausage_spawn::Command;
|
|
use std::{
|
|
io::{self, Write},
|
|
path::PathBuf,
|
|
};
|
|
|
|
enum Predicate {
|
|
Name(globset::GlobMatcher),
|
|
Path(globset::GlobMatcher),
|
|
Type(String),
|
|
Newer(std::time::SystemTime),
|
|
Empty,
|
|
Prune,
|
|
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;
|
|
}
|
|
if roots.is_empty() {
|
|
roots.push(".".into());
|
|
}
|
|
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;
|
|
}
|
|
"-a" | "-and" => continue,
|
|
"-prune" => Predicate::Prune,
|
|
"-empty" => Predicate::Empty,
|
|
"-print" | "-print0" => {
|
|
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;
|
|
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;
|
|
}
|
|
_ => {
|
|
mindepth = value.parse()?;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
"-exec" => {
|
|
let start = i;
|
|
while i < args.len() && args[i] != ";" {
|
|
i += 1;
|
|
}
|
|
if i == args.len() || i == start {
|
|
return Err("-exec requires a command and ;".into());
|
|
}
|
|
explicit_action = true;
|
|
let argv = args[start..i].to_vec();
|
|
i += 1;
|
|
Predicate::Exec(argv)
|
|
}
|
|
_ => return Err(format!("unsupported predicate: {flag}").into()),
|
|
};
|
|
groups.last_mut().unwrap().push(predicate);
|
|
}
|
|
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();
|
|
while let Some(entry) = walk.next() {
|
|
let entry = match entry {
|
|
Ok(e) => e,
|
|
Err(e) => {
|
|
eprintln!("find: {e}");
|
|
status = 1;
|
|
continue;
|
|
}
|
|
};
|
|
if entry.depth() < 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;
|
|
}
|
|
}
|
|
if prune && entry.file_type().is_dir() {
|
|
walk.skip_current_dir();
|
|
}
|
|
if matches && !explicit_action {
|
|
out.write_all(entry.path().as_os_str().as_encoded_bytes())?;
|
|
out.write_all(b"\n")?;
|
|
}
|
|
}
|
|
}
|
|
out.flush()?;
|
|
Ok(status)
|
|
}
|
|
fn main() {
|
|
std::process::exit(match run() {
|
|
Ok(code) => code,
|
|
Err(e) => {
|
|
eprintln!("find: {e}");
|
|
1
|
|
}
|
|
});
|
|
}
|