//! For parsing arguments use super::*; use std::collections::{HashMap, HashSet}; use std::mem::Discriminant; use std::fmt; #[cfg(feature="inspect")] use config::OutputSerialisationMode; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum InspectKind { Treemap(Option<(u64, u64)>), } impl fmt::Display for InspectKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Treemap(None) => write!(f, "treemap"), Self::Treemap(Some((x,y))) => write!(f, "treemap:{}:{}", x, y), // Width and height. } } } impl std::str::FromStr for InspectKind { type Err = eyre::Report; fn from_str(s: &str) -> Result { todo!() } } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Argument { ModeChangeHelp, LimitConcMaxProc, LimitConc(NonZeroUsize), UnlimitConc, Save(String), SaveStdout, SaveRaw(String), SaveRawStdout, LimitRecurse(NonZeroUsize), UnlimitRecurse, LogVerbose, LogQuiet, LogSilent, StopReading, Inspect(InspectKind), Input(String), } /// Kinds of modes of operation for the program. /// /// These map to `super::Mode`. #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd, Copy)] #[non_exhaustive] enum ModeKind { Normal, Help } impl Default for ModeKind { #[inline] fn default() -> Self { Self::Normal } } impl Argument { /// What mode does this argument change to, if any? fn mode_change_kind(&self) -> Option { Some(match self { Self::ModeChangeHelp => ModeKind::Help, _ => return None, }) } /// Insert this `Argument` into config pub fn insert_into_cfg(self, cfg: &mut Config) { use Argument::*; match self { Inspect(InspectKind::Treemap(None)) => cfg.inspection.treemap = Some((640, 480)), Inspect(InspectKind::Treemap(x)) => cfg.inspection.treemap = x, LimitConcMaxProc => cfg.max_tasks = config::max_tasks_cpus(), LimitConc(max) => cfg.max_tasks = Some(max), UnlimitConc => cfg.max_tasks = None, #[cfg(feature="inspect")] Save(output) => cfg.serialise_output = Some(OutputSerialisationMode::File(output.into())), #[cfg(feature="inspect")] SaveStdout => cfg.serialise_output = Some(OutputSerialisationMode::Stdout), #[cfg(feature="inspect")] SaveRaw(output) => { cfg_if! { if #[cfg(feature="prealloc")] { cfg.serialise_output = Some(OutputSerialisationMode::PreallocFile(output.into())); } else { cfg.serialise_output = Some(OutputSerialisationMode::RawFile(output.into())); } } }, #[cfg(feature="inspect")] SaveRawStdout => cfg.serialise_output = Some(OutputSerialisationMode::RawStdout), LimitRecurse(limit) => cfg.recursive = if limit.get() == 1 { config::Recursion::None } else { config::Recursion::Limited(limit) }, UnlimitRecurse => cfg.recursive = config::Recursion::Unlimited, LogVerbose => cfg.output_level = config::OutputLevel::Verbose, LogQuiet => cfg.output_level = config::OutputLevel::Quiet, LogSilent => cfg.output_level = config::OutputLevel::Silent, Input(path) => cfg.paths.push(path.into()), _ => (), //unreachable()! // Do nothing instead of panic. } } } impl fmt::Display for Argument { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use Argument::*; match self { Inspect(ins) => write!(f, "--inspect {}", ins), ModeChangeHelp => write!(f, "--help"), LimitConcMaxProc => write!(f, "-m"), LimitConc(limit) => write!(f, "--threads {}", limit), UnlimitConc => write!(f, "-M (--threads 0)"), Save(s) => write!(f, "--save {:?}", s), SaveStdout => write!(f, "-D"), SaveRaw(s) => write!(f, "--save-raw {:?}", s), SaveRawStdout => write!(f, "-R"), LimitRecurse(rec) => write!(f, "--recursive {}", rec), UnlimitRecurse => write!(f, "-r (--recursive 0)"), LogVerbose => write!(f, "-v"), LogQuiet => write!(f, "-q"), LogSilent => write!(f, "-Q"), StopReading => write!(f, "-"), Input(input) => write!(f, "<{}>", input), } } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)] enum MX { None, Itself, All, Only(Discriminant), Many(&'static [Discriminant]), } impl Default for MX { #[inline] fn default() -> Self { Self::Itself } } impl MX { /// Is this argument discriminant mutually exclusive with this other argument? pub fn is_mx(&self, this: Discriminant, other: &Argument) -> bool { use std::mem::discriminant; let other = discriminant(other); match self { Self::Itself if other == this => true, Self::All => true, Self::Only(disc) if other == *disc => true, Self::Many(discs) if discs.contains(&other) => true, _ => false, } } } impl Argument { /// Is this `Argument` mutually exclusive with another? pub fn is_mx_with(&self, other: &Self) -> bool { use std::mem::discriminant; lazy_static! { static ref MX_REF: HashMap, MaybeVec> = { let mut out = HashMap::new(); macro_rules! mx { (@) => { std::iter::empty() }; (@ self $($tt:tt)*) => { iter![MX::Itself].chain(mx!(@ $($tt)*)) }; (@ [$inner:expr] $($tt:tt)*) => { iter![MX::Only(discriminant(&$inner))].chain(mx!(@ $($tt)*)) }; (@ [$($inner:expr),*] $($tt:tt)*) => { iter![MX::Many(vec![$(discriminant(&$inner)),*].leak())].chain(mx!(@ $($tt)*)) }; (@ $ident:ident $($tt:tt)*) => { iter![MX::$ident].chain(mx!(@ $($tt)*)) }; ($disc:expr => $($tt:tt)*) => { out.insert(discriminant(&$disc), mx!(@ $($tt)*).collect()); }; } mx!(Argument::ModeChangeHelp => All); mx!(Argument::LimitConcMaxProc => self [Argument::UnlimitConc, Argument::LimitConc(unsafe{NonZeroUsize::new_unchecked(1)})]); mx!(Argument::UnlimitConc => self [Argument::LimitConcMaxProc, Argument::LimitConc(unsafe{NonZeroUsize::new_unchecked(1)})]); mx!(Argument::LimitConc(unsafe{NonZeroUsize::new_unchecked(1)}) => self [Argument::LimitConcMaxProc, Argument::UnlimitConc]); mx!(Argument::Save(String::default()) => self [Argument::SaveStdout, Argument::SaveRaw(Default::default()), Argument::SaveRawStdout]); mx!(Argument::SaveStdout => self [Argument::Save(String::default()), Argument::SaveRaw(Default::default()), Argument::SaveRawStdout]); mx!(Argument::SaveRaw(Default::default()) => self [Argument::Save(String::default()), Argument::SaveStdout, Argument::SaveRawStdout]); mx!(Argument::SaveRawStdout => self [Argument::Save(String::default()), Argument::SaveRaw(String::default()), Argument::SaveStdout]); mx!(Argument::LimitRecurse(unsafe{NonZeroUsize::new_unchecked(1)}) => self [Argument::UnlimitRecurse]); mx!(Argument::UnlimitRecurse => self [Argument::LimitRecurse(unsafe{NonZeroUsize::new_unchecked(1)})]); mx!(Argument::LogVerbose => self [Argument::LogQuiet, Argument::LogSilent]); mx!(Argument::LogQuiet => self [Argument::LogVerbose, Argument::LogSilent]); mx!(Argument::LogSilent => self [Argument::LogQuiet, Argument::LogVerbose]); mx!(Argument::StopReading => All); mx!(Argument::Input(String::default()) => None); out }; } let this = discriminant(self); match MX_REF.get(&this) { Some(mx) if mx.iter().filter(|mx| mx.is_mx(this, other)).next().is_some() => true, _ => false, } } } /// Should we continue parsing and/or reading arguments? #[derive(Debug, Clone, PartialEq, Eq)] pub enum Continue { /// Keep parsing the arguments Yes, /// Stop parsing arguments, add the rest of args as `Input`s No, /// On mode change, we don't need to parse the rest of the argument. Stop reading entirely, and optionally return the last one here, which must be a mode change argument. /// /// Returning this when the contained value is `Some` immediately terminates parsing and precedes to mode-switch. However, if it is `None`, parsing of chained short args is allowed to continue, although `Abort(None)` will be returned at the end regardless of subsequent `Continue` results from that change (unless one is an `Abort(Some(_))`, which immediately returns itself.) // Box `Argument` to reduce the size of `Continue`, as it is returned from functions often and when its value is set to `Some` it will always be the last `Argument` processed anyway and the only one to be boxed here at all. //TODO: Deprecate the early return of an `Argument` here. Either change it to `Mode`, or have no early return. Mode change happens at the bottom in `into_mode` now. Abort(Option>), } impl Continue { /// Should we keep *parsing* args? #[inline] pub fn keep_reading(&self) -> bool { if let Self::Yes = self { true } else { false } } /// Is this an abort? #[inline] pub fn is_abort(&self) -> bool { if let Self::Abort(_) = self { true } else { false } } } impl Default for Continue { #[inline] fn default() -> Self { Self::Yes } } impl From for Continue { fn from(from: bool) -> Self { if from { Self::Yes } else { Self::No } } } pub type Output = HashSet; #[inline] const fn suggestion_intended_arg() -> &'static str { "If this was intended as a path instead of an option, use option `-` before it." } fn save_output(output: &mut Output, item: Argument) -> eyre::Result<()> { if let Some(mx) = output.iter().filter(|arg| item.is_mx_with(arg)).next() { return Err(eyre!("Arguments are mutually exclusive")) .with_section(|| item.header("Trying to addargument ")) .with_section(|| mx.to_string().header("Which is mutually exclusive with previously added")); } output.insert(item); //TODO: Warn when adding duplicate? Ok(()) } fn parse_single(_args: &mut I, output: &mut Output, this: char) -> eyre::Result where I: Iterator { let item = match this { 'r' => Argument::UnlimitRecurse, #[cfg(feature="inspect")] 'D' => Argument::SaveStdout, #[cfg(feature="inspect")] 'R' => Argument::SaveRawStdout, 'v' => Argument::LogVerbose, 'q' => Argument::LogQuiet, 'Q' => Argument::LogSilent, 'm' => Argument::LimitConcMaxProc, 'M' => Argument::UnlimitConc, unknown => { return Err(eyre!("Unknown short argument {:?}", unknown)) .with_suggestion(suggestion_intended_arg.clone()); }, }; save_output(output, item) .with_section(|| this.header("Short argument was"))?; Ok(Continue::Yes) } /// Consume this iterator into `Input`s pub fn consume(args: I, output: &mut Output) where I: IntoIterator { output.extend(args.into_iter().map(Argument::Input)); } pub fn parse_next(args: &mut I, output: &mut Output, this: String) -> eyre::Result where I: Iterator { let mut keep_reading = Continue::Yes; let item = match this.trim() { "--inspect" => { let ins = args.next().ok_or(eyre!("`--inspect` expects a parameter")) .with_suggestion(suggestion_intended_arg.clone())?; Argument::Inspect(ins.parse().wrap_err(eyre!("Failed to parse parameter for `--inspect`"))?) }, " --threads" => { let max = args.next().ok_or(eyre!("`--threads` expects a parameter")) .with_suggestion(suggestion_intended_arg.clone())?; match NonZeroUsize::new(max.parse::() .wrap_err(eyre!("`--threads` expects a non-negative number")) .with_suggestion(suggestion_intended_arg.clone()) .with_section(move || max.header("Parameter given was"))?) { Some(max) => Argument::LimitConc(max), None => Argument::UnlimitConc, } }, "--recursive" => { let max = args.next().ok_or(eyre!("`--recursive` expects a parameter")) .with_suggestion(suggestion_intended_arg.clone())?; match NonZeroUsize::new(max.parse::().wrap_err(eyre!("`--recursive` expects a non-negative number")) .with_suggestion(suggestion_intended_arg.clone()) .with_section(move || max.header("Parameter given was"))?) { Some(x) => Argument::LimitRecurse(x), None => Argument::UnlimitRecurse, } }, "--help" => { return Ok(Continue::Abort(Some(Box::new(Mode::Help)))); }, "-" => { return Ok(Continue::No); }, #[cfg(feature="inspect")] "--save" => { let file = args.next().ok_or(eyre!("`--save` expects a parameter")) .with_suggestion(suggestion_intended_arg.clone())?; Argument::Save(file) }, #[cfg(feature="inspect")] "--save-raw" => { let file = args.next().ok_or(eyre!("`--save` expects a parameter")) .with_suggestion(suggestion_intended_arg.clone())?; Argument::SaveRaw(file) }, single if single.starts_with("-") => { for ch in single.chars().skip(1) { match parse_single(args, output, ch) .wrap_err(eyre!("Error parsing short argument")) .with_section(|| this.clone().header("Full short argument chain was"))? { abort @ Continue::Abort(Some(_)) => return Ok(abort), x @ Continue::No | x @ Continue::Abort(_) if !x.is_abort() => keep_reading = x, _ => (), } } return Ok(keep_reading); }, _ => { keep_reading = Continue::No; Argument::Input(this) } }; save_output(output, item)?; Ok(keep_reading) } /// Converts parsed argument lists into a respective mode. /// /// # Notes /// These functions assume the mode has already been correctly calculated to be the mode pertaining to that function. mod modes { use super::*; use config::Config; /// Consume a parsed list of arguments in `Normal` mode into a `Normal` mode `Config` object. pub fn normal(args: Output) -> eyre::Result { let mut cfg = Config::default(); for arg in args.into_iter() { arg.insert_into_cfg(&mut cfg); } Ok(cfg) } } /// Consume this parsed list of arguments into a `Mode` and return it pub fn into_mode(args: Output) -> eyre::Result { let mut mode_kind = ModeKind::default(); //Normal. for arg in args.iter() { //find any mode change Argument (with `Argument::mode_change_kind()`) in `args`, changing `mode_kind` in turn. There should be at most 1. if let Some(mode) = arg.mode_change_kind() { mode_kind = mode; break; } } //pass `args` to the respective mode generation function in mode `modes`, and wrap that mode around its return value. match mode_kind { ModeKind::Normal => modes::normal(args).map(Mode::Normal), ModeKind::Help => Ok(Mode::Help), } }