diff options
author | Ben Bridle <ben@derelict.engineering> | 2025-02-03 17:50:10 +1300 |
---|---|---|
committer | Ben Bridle <ben@derelict.engineering> | 2025-02-03 17:50:10 +1300 |
commit | 5277fd9c56619d1fcd4776968b851ea534526435 (patch) | |
tree | c4f8f95d8b8c9a8e3e8e4fda64f2817181d0161d /src | |
download | switchboard-5277fd9c56619d1fcd4776968b851ea534526435.zip |
Initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 113 | ||||
-rw-r--r-- | src/query.rs | 180 |
2 files changed, 293 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e25725d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,113 @@ +mod query; +pub use query::*; + + +pub enum SwitchName { + Short(char), + Long(String), +} + + +pub struct Switchboard { + // First positional argument. + pub program: String, + // Positional arguments, stored in reverse order. + pub positional: Vec<String>, + // Named arguments, stored in forward order. + pub switches: Vec<(SwitchName, Option<String>)>, + // All arguments following a '--' token, stored in forward order. + pub unprocessed: Option<Vec<String>>, +} + +impl Switchboard { + pub fn parse(mut args: Vec<String>) -> Self { + let mut positional = Vec::new(); + let mut switches = Vec::new(); + let mut unprocessed = None; + args.reverse(); + + while let Some(arg) = args.pop() { + if arg.is_empty() { + continue; + } else if arg == "--" { + args.reverse(); + unprocessed = Some(args); + break; + } else if let Some(arg) = arg.strip_prefix("--") { + let (name, value) = match arg.split_once("=") { + Some((name, "")) => (name, None), + Some((name, value)) => (name, Some(value.to_string())), + None => (arg, None), + }; + if name.is_empty() { continue } + switches.push((SwitchName::Long(name.to_string()), value)); + } else if let Some(arg) = arg.strip_prefix("-") { + let (name, value) = match arg.split_once("=") { + Some((name, "")) => (name, None), + Some((name, value)) => (name, Some(value.to_string())), + None => (arg, None), + }; + let chars: Vec<char> = name.chars().collect(); + if chars.len() != 1 { continue } + switches.push((SwitchName::Short(chars[0]), value)); + } else { + positional.push(arg) + } + } + // Reverse order of positional arguments and move program name. + positional.reverse(); + let program = positional.pop().unwrap(); + Self { + program, positional, switches, unprocessed, + } + } + + pub fn from_env() -> Self { + let mut args = Vec::new(); + for arg_os in std::env::args_os() { + args.push(arg_os.to_string_lossy().to_string()); + } + Self::parse(args) + } + + pub fn named(&mut self, name: &str) -> NamedSwitchQuery { + validate_name(name); + NamedSwitchQuery { + switchboard: self, + name: name.to_string(), + short: None, + default: None, + quick: None, + } + } + + pub fn positional(&mut self, name: &str) -> PositionalSwitchQuery { + validate_name(name); + PositionalSwitchQuery { + switchboard: self, + name: name.to_string(), + default: None, + } + } + + /// Check the next positional argument without consuming it. + pub fn peek(&self) -> Option<&str> { + self.positional.last().map(|p| p.as_str()) + } + + /// Consume the next positional argument. + pub fn pop(&mut self) { + self.positional.pop(); + } +} + + +pub enum SwitchboardError { +} + + +fn validate_name(name: &str) { + if !name.chars().all(|c| c.is_ascii_lowercase() || c.is_digit(10) || c == '-') { + panic!("Invalid name for argument: {name:?}"); + } +} diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 0000000..4109488 --- /dev/null +++ b/src/query.rs @@ -0,0 +1,180 @@ +use crate::*; + +use log::fatal; + +use std::path::PathBuf; + + +pub struct NamedSwitchQuery<'a> { + /// The borrowed switchboard instance. + pub switchboard: &'a mut Switchboard, + /// Long switch name. + pub name: String, + /// Short switch name. + pub short: Option<char>, + /// The default value if the switch hasn't been provided. + pub default: Option<String>, + /// The default value if the switch has been provided with no value. + pub quick: Option<String>, +} + +impl NamedSwitchQuery<'_> { + pub fn short(mut self, short: char) -> Self { + if !short.is_ascii_alphanumeric() { + panic!("Invalid short name for argument: {short:?}"); + } + self.short = Some(short); + self + } + + pub fn default(mut self, value: &str) -> Self { + self.default = Some(value.to_string()); + self + } + + pub fn quick(mut self, value: &str) -> Self { + self.quick = Some(value.to_string()); + self + } + + fn debug_name(&self) -> String { + let mut debug_name = format!("--{}", self.name); + if let Some(short) = self.short { + debug_name.push_str(&format!(" (-{})", short)); + } + debug_name + } +} + + +pub struct PositionalSwitchQuery<'a> { + /// The borrowed switchboard instance. + pub switchboard: &'a mut Switchboard, + /// The display name of this argument. + pub name: String, + /// The default value if the switch hasn't been provided. + pub default: Option<String>, +} + +impl PositionalSwitchQuery<'_> { + pub fn default(mut self, value: &str) -> Self { + self.default = Some(value.to_string()); + self + } +} + + +macro_rules! as_number { + ($type:ty, $name:expr, $error:ty) => { + paste::paste! { + fn [< as_ $type >](&mut self) -> $type { + self.[< as_ $type _opt >]().unwrap_or_else(|| self.missing("number")) + } + fn [< as_ $type _opt>](&mut self) -> Option<$type> { + self.get_value().map(|v| v.trim().parse().unwrap_or_else( + |e: $error| self.error(&v, $name, e.to_string()))) + } + } + }; +} + + +pub trait SwitchQuery { + fn get_name(&self) -> String; + + fn get_value(&mut self) -> Option<String>; + + fn error(&self, value: &str, ty: &str, err: String) -> ! { + let name = self.get_name(); + fatal!("The {name} with value {value:?} could not be parsed as {ty}: {err}"); + } + + fn missing(&self, ty: &str) -> ! { + let name = self.get_name(); + fatal!("The required {name} that takes a {ty} value was not provided"); + } + + fn as_bool(&mut self) -> bool { + if let Some(value) = self.get_value() { + match value.to_lowercase().as_str() { + "y"|"yes"|"t"|"true" => true, + "n"|"no" |"f"|"false" => false, + _ => true, + } + } else { + false + } + } + + fn as_path(&mut self) -> PathBuf { + self.as_path_opt().unwrap_or_else(|| self.missing("path")) + } + fn as_path_opt(&mut self) -> Option<PathBuf> { + if let Some(value) = self.get_value() { + if !value.is_empty() { + return Some(PathBuf::from(value)); + } + } + return None; + } + + fn as_string(&mut self) -> String { + self.as_string_opt().unwrap_or_else(|| self.missing("string")) + } + fn as_string_opt(&mut self) -> Option<String> { + self.get_value() + } + + as_number!{ f32, "f32" , std::num::ParseFloatError } + as_number!{ f64, "f64" , std::num::ParseFloatError } + as_number!{ u8, "u8" , std::num::ParseIntError } + as_number!{ u16, "u16" , std::num::ParseIntError } + as_number!{ u32, "u32" , std::num::ParseIntError } + as_number!{ u64, "u64" , std::num::ParseIntError } + as_number!{ usize, "usize", std::num::ParseIntError } + as_number!{ i8, "i8" , std::num::ParseIntError } + as_number!{ i16, "i16" , std::num::ParseIntError } + as_number!{ i32, "i32" , std::num::ParseIntError } + as_number!{ i64, "i64" , std::num::ParseIntError } + as_number!{ isize, "isize", std::num::ParseIntError } +} + + +impl SwitchQuery for NamedSwitchQuery<'_> { + fn get_name(&self) -> String { + format!("argument {}", self.debug_name()) + } + + fn get_value(&mut self) -> Option<String> { + // Find all matches first. + let mut matches = Vec::new(); + for (i, switch) in self.switchboard.switches.iter().enumerate() { + match &switch.0 { + SwitchName::Short(other) => if let Some(short) = self.short { + if short == *other { matches.push(i) } } + SwitchName::Long(other) => if self.name == *other { + matches.push(i); }, + }; + } + match matches.len() { + 0 => self.default.clone(), + 1 => Some( + self.switchboard.switches.remove(matches[0]).1 + .or_else(|| self.quick.clone()) + .unwrap_or_else(String::new) + ), + _ => fatal!("The argument {} was passed more than once", self.debug_name()), + } + } +} + + +impl SwitchQuery for PositionalSwitchQuery<'_> { + fn get_name(&self) -> String { + format!("positional argument {:?}", self.name) + } + + fn get_value(&mut self) -> Option<String> { + self.switchboard.positional.pop().or_else(|| self.default.clone()) + } +} |