summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorBen Bridle <ben@derelict.engineering>2025-02-03 17:50:10 +1300
committerBen Bridle <ben@derelict.engineering>2025-02-03 17:50:10 +1300
commit5277fd9c56619d1fcd4776968b851ea534526435 (patch)
treec4f8f95d8b8c9a8e3e8e4fda64f2817181d0161d /src
downloadswitchboard-5277fd9c56619d1fcd4776968b851ea534526435.zip
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs113
-rw-r--r--src/query.rs180
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())
+ }
+}