//! 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), } fn run() -> Result> { 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 } }); }