//! A deliberately tiny argument parser. //! //! Four subcommands do not justify a `clap` dependency and its build time. The //! only subtlety is that some long options take a value (`--pid 165925`) and //! some are bare booleans (`--utf16`). A parser cannot tell those apart from //! the token stream alone, so each subcommand declares which of its options //! take a value and we look the name up in that list. use std::collections::HashMap; pub struct Args { opts: HashMap>, pub positional: Vec, } #[derive(Debug)] pub struct ArgError(pub String); impl std::fmt::Display for ArgError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Args { /// `value_flags` lists the long option names that consume the following /// token as their value. Everything else beginning with `--` is a boolean. /// `--name=value` is always accepted regardless of the list. pub fn parse>( argv: I, value_flags: &[&str], ) -> Result { let mut opts: HashMap> = HashMap::new(); let mut positional = Vec::new(); let mut it = argv.peekable(); while let Some(tok) = it.next() { if let Some(rest) = tok.strip_prefix("--") { if rest.is_empty() { // A bare `--` ends option parsing; the rest is positional. positional.extend(it.by_ref()); break; } if let Some((name, value)) = rest.split_once('=') { opts.insert(name.to_string(), Some(value.to_string())); } else if value_flags.contains(&rest) { let value = it .next() .ok_or_else(|| ArgError(format!("--{rest} needs a value")))?; opts.insert(rest.to_string(), Some(value)); } else { opts.insert(rest.to_string(), None); } } else { positional.push(tok); } } Ok(Args { opts, positional }) } pub fn has(&self, name: &str) -> bool { self.opts.contains_key(name) } pub fn value(&self, name: &str) -> Option<&str> { self.opts.get(name).and_then(|v| v.as_deref()) } pub fn parse_value(&self, name: &str) -> Result, ArgError> { match self.value(name) { None => Ok(None), Some(raw) => raw .parse::() .map(Some) .map_err(|_| ArgError(format!("could not parse --{name} value {raw:?}"))), } } /// Reject typos instead of silently ignoring them. `futmem find --acii foo` /// should not quietly scan for nothing. pub fn reject_unknown(&self, known: &[&str]) -> Result<(), ArgError> { for name in self.opts.keys() { if !known.contains(&name.as_str()) { return Err(ArgError(format!("unknown option --{name}"))); } } Ok(()) } } /// Parse `0x1234`, `1234` (hex assumed when the `0x` prefix is present, /// decimal otherwise) into a virtual address. pub fn parse_addr(raw: &str) -> Result { let cleaned = raw.replace('_', ""); let parsed = match cleaned .strip_prefix("0x") .or_else(|| cleaned.strip_prefix("0X")) { Some(hex) => u64::from_str_radix(hex, 16), // Bare addresses in this project are always written in hex // (`6ffffc140000`), so try hex first and fall back to decimal only for // values that are unambiguous. None => u64::from_str_radix(&cleaned, 16).or_else(|_| cleaned.parse::()), }; parsed.map_err(|_| ArgError(format!("bad address {raw:?}"))) } /// Parse a length: `4096`, `0x1000`, `16k`, `2m`. pub fn parse_len(raw: &str) -> Result { let lower = raw.to_ascii_lowercase(); let (body, mult) = match lower.strip_suffix('k') { Some(b) => (b, 1024u64), None => match lower.strip_suffix('m') { Some(b) => (b, 1024 * 1024), None => (lower.as_str(), 1), }, }; let n = match body.strip_prefix("0x") { Some(hex) => u64::from_str_radix(hex, 16), None => body.parse::(), } .map_err(|_| ArgError(format!("bad length {raw:?}")))?; Ok(n * mult) }