Files
sausage/userland/findutils/src/find.rs
T

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