//! Arg parsing use super::*; use std::{ ffi::OsStr, path::{ Path, PathBuf, }, borrow::Cow, fmt, }; use tokio::{ sync::{ mpsc, }, }; use futures::{ stream::{self, Stream, BoxStream, StreamExt,}, }; /// Parsed command-line args #[derive(Debug, Clone)] pub struct Args { pub delim: u8, pub reverse: bool, pub walker: walk::Config, pub worker: work::Config, paths: Option>, } impl Default for Args { #[inline] fn default() -> Self { Self { delim: b'\n', reverse: false, walker: Default::default(), worker: Default::default(), paths: None, } } } impl Args { /// Paths as an async stream /// /// # Non-immediate /// When input paths come from `stdin`, the output stream will be non-immediate. pub fn paths(&self) -> BoxStream<'_, Cow<'_, Path>> { if let Some(paths) = self.paths.as_ref() { stream::iter(paths.iter().map(|x| Cow::Borrowed(Path::new(x)))).boxed() } else { let (tx, rx) = mpsc::channel(128); let read_chr = self.delim; tokio::spawn(async move { use tokio::io::{ self, AsyncReadExt, AsyncBufReadExt }; let mut stdin = { tokio::io::BufReader::new(io::stdin()) }; let mut buf = Vec::with_capacity(1024); loop { buf.clear(); use std::os::unix::prelude::*; let n = match stdin.read_until(read_chr, &mut buf).await { Ok(n) => n, Err(e) => { error!("paths: Failed to read input line: {}", e); break; }, }; trace!("paths: buffer: {:?}", &buf[..]); if n == 0 { trace!("paths: Stdin exhausted. Exiting."); break; } let path_bytes = &buf[..n]; let path_bytes = if path_bytes.len() == 1 { trace!("paths: Ignoring empty line. Yielding then continuing."); tokio::task::yield_now().await; continue; } else if path_bytes[n-1] == read_chr { &path_bytes[.. (path_bytes.len()-1)] } else { path_bytes }; let path = Path::new(OsStr::from_bytes(path_bytes)); trace!("Read path {:?}", path); if path.exists() { if tx.send(path.to_owned()).await.is_err() { trace!("paths: Stream dropped, cancelling stdin read."); break; } } } }); tokio_stream::wrappers::ReceiverStream::new(rx).map(|x| Cow::Owned(x)).boxed() } } } #[derive(Debug, Clone)] pub enum Mode { Normal(Args), Help, } #[inline] pub fn parse_args() -> eyre::Result { //return Ok(Args { paths: None }); parse(std::env::args().skip(1)) .with_context(|| format!("{:?}", std::env::args().collect::>()).header("ARGV was")) } /// The executable name, if readable from argv as a valid UTF8 string. /// /// If not readable, the project name will be returned. #[inline] pub fn prog_name() -> &'static str { lazy_static! { static ref PROG_NAME: &'static str = std::env::args().next().map(|x| &*Box::leak(x.into_boxed_str())).unwrap_or(env!("CARGO_PKG_NAME")); } *PROG_NAME } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)] enum Arg<'a> { Long(&'a str), Short(&'a [u8]), ShortSingle(u8), } impl<'a> Arg<'a> { #[inline] pub fn as_long(&self) -> Option<&'a str> { match self { Self::Long(l) => Some(l), _ => None, } } #[inline] pub fn as_short_ascii(&self) -> Option<&'a [u8]> { match self { Self::Short(l) => Some(l), //Self::ShortSingle(s) => Some(&[*s]), _ => None, } } #[inline] pub fn split_short(&self) -> Option + 'a> { self.as_short_ascii().map(|x| std::str::from_utf8(x).ok() /* XXX: Silent failure is not a good idea.. We should return an error (or maybe just panic? if there's invalid utf8 here, it shouldn't happen)*/).flatten().map(|s| s.chars()) } #[inline] pub fn split_short_ascii(&self) -> Option + 'a> { self.as_short_ascii().map(|opt| opt.into_iter().copied()) } #[inline] pub fn explode(self) -> impl Iterator> + 'a { std::iter::once(self) .chain(std::iter::once(if let Self::Short(short) = self { Some(short.into_iter().copied().map(|x| Arg::ShortSingle(x))) } else { None }) .flat_map(std::convert::identity).flatten()) } #[inline] pub fn is_any<'b: 'a, I: 'b, A>(&self, these: I) -> bool where I: IntoIterator, A: Into> + 'b { let iter: Vec<_> = these.into_iter().map(Into::into).map(|x| x.explode()).flatten().collect(); for split in self.explode() { if iter.iter().any(|arg| arg == &split) { return true; } } false } #[inline(always)] pub fn is_long(&self) -> bool { self.as_long().is_some() } #[inline(always)] pub fn is_short(&self) -> bool { !self.is_long() } } impl<'a> fmt::Display for Arg<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Long(s) => write!(f, "--{s}"), Self::Short(short) => write!(f, "-{}", std::str::from_utf8(short).unwrap()), Self::ShortSingle(one) => write!(f, "-{}", *one as char), } } } impl<'a> From<&'a [u8]> for Arg<'a> { #[inline] fn from(from: &'a [u8]) -> Self { Self::Short(from) } } impl<'a, const N: usize> From<&'a [u8; N]> for Arg<'a> { #[inline] fn from(from: &'a [u8; N]) -> Self { Self::Short(&from[..]) } } impl<'a> From<&'a str> for Arg<'a> { #[inline] fn from(from: &'a str) -> Self { Self::Long(from) } } impl From for Arg<'static> { #[inline] fn from(from: u8) -> Self { Self::ShortSingle(from) } } #[inline] fn parse_single<'a, I: ?Sized + 'a>(input: Arg<'a>, args: &mut I, output: &mut Args) -> eyre::Result> where I: Iterator { macro_rules! take { ($fmt:literal $(, $ag:expr)*) => { match args.next() { Some(n) => n, None => return Err(eyre!($fmt $(, $ag)*)), } }; () => { take!("`{}` expects an argument", &input) } } macro_rules! args { ($($arg:expr),*) => { [$(Arg::from($arg)),*] }; } // Modes // // --help if input == Arg::Long("help") { return Ok(Some(Mode::Help)); } // Normal // // -r, --recursive if input.is_any(args![b'r', "recursive"]) { output.walker.recursion_depth = if input.is_long() { let limit = take!(); let limit: usize = (&limit).parse().wrap_err("`--recursive` expects a positive integer") .with_section(move || limit.header("Invalid parameter was"))?; match limit { 0 => None, 1 => { warn!("`--recursive 1` is a no-op, did you mean `--recursive 2`?"); Some(1) }, n => Some(n), } } else { None }; } // -a, -m, -c, -b, --{a,m,c,b}time if input.is_any(args![b'a', "atime"]) { output.worker.by = work::OrderBy::AccessTime; } else if input.is_any(args![b'c', "ctime"]) { output.worker.by = work::OrderBy::ChangeTime; } else if input.is_any(args![b'm', "mtime"]) { output.worker.by = work::OrderBy::ModifiedTime; } else if input.is_any(args![b'b', "btime"]) { output.worker.by = work::OrderBy::BirthTime; } // -z, -0, --nul, // -I, --delim |ifs 'delim: { output.delim = if input.is_any(args![b"z0", "nul"]) { 0u8 } else if input.is_any(args![b'I', "delim"]) { fn read_ifs_as_byte() -> eyre::Result { let Some(ifs) = std::env::var_os("IFS") else { return Err(eyre!("IFS env-var not set")); }; use std::os::unix::prelude::*; ifs.as_bytes().first().copied().ok_or(eyre!("IFS env-var empty")) } if input.is_long() { let val = take!(); match val.as_bytes() { [] => return Err(eyre!("Line seperator cannot be empty").with_suggestion(|| "--delim ifs|".header("Usage is"))), b"ifs" | b"IFS" => { read_ifs_as_byte().wrap_err(eyre!("Failed to read line seperator from IFS env var"))? } [de] => *de, [de, rest @ ..] => { warn!("Specified more than one byte for line seperator. Ignoring other {} bytes", rest.len()); *de }, } } else { // Read IFS read_ifs_as_byte().wrap_err(eyre!("Failed to read line seperator from IFS env var"))? } } else { // No change break 'delim; }; }; // -n, --reverse output.reverse = input.is_any(args![b'n', "reverse"]); // -P, -p, --parallel cpus| // -1 if input.is_any(args![b'P', b'p', "parallel"]) { if input.is_long() { let mut num = take!(); if let Ok(n) = num.parse() { output.walker.max_walkers = std::num::NonZeroUsize::new(n); } else { num.make_ascii_lowercase(); match &num[..] { "cpus" => output.walker.max_walkers = std::num::NonZeroUsize::new(*walk::NUM_CPUS), _ => return Err(eyre!("`--parallel` expects a positive integer or the string 'cpus'")).with_context(move || num.header("Invalid parameter was")), } } } else { output.walker.max_walkers = if input.is_any(*b"P") { None } else { std::num::NonZeroUsize::new(*walk::NUM_CPUS) }; } } else if input.is_any(args![b'1']) { output.walker.max_walkers = std::num::NonZeroUsize::new(1); } Ok(None) } fn parse(args: impl IntoIterator) -> eyre::Result { let mut output = Args::default(); let mut args = args.into_iter().fuse(); let mut rest = Vec::new(); while let Some(current) = args.next() { macro_rules! single { ($input:expr) => { { let input = Arg::from($input); if let Some(mode) = parse_single($input, &mut args, &mut output) .wrap_err(eyre!("Parsing error for argument '{}'", &input)) .with_section(|| current.clone().header("Current arg was"))? { return Ok(mode); } } }; } match current.as_bytes() { b"-" | b"--" => break, [b'-', b'-', ..] => { // Long opt single!(Arg::Long(¤t[2..])); }, [b'-', short @ ..] => { // Short opts single!(Arg::Short(short)) }, _ => { // Not an opt, a path. rest.push(PathBuf::from(current)); break; }, } } rest.extend(args.map(Into::into)); output.paths = match rest { empty if empty.is_empty() => None, rest => Some(rest), }; Ok(Mode::Normal(output)) } //TODO: fn parse(args: impl IntoIterator) -> eyre::Result