diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 112 | ||||
-rw-r--r-- | src/main.rs | 60 | ||||
-rw-r--r-- | src/query.rs | 236 | ||||
-rw-r--r-- | src/switchboard.rs | 145 | ||||
-rw-r--r-- | src/value.rs | 112 |
5 files changed, 454 insertions, 211 deletions
@@ -1,5 +1,12 @@ mod query; +mod switchboard; +mod value; + pub use query::*; +pub use switchboard::*; +pub use value::*; + +use log::*; pub enum SwitchName { @@ -7,107 +14,24 @@ pub enum SwitchName { 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, +impl std::fmt::Display for SwitchName { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + SwitchName::Short(c) => write!(f, "-{c}"), + SwitchName::Long(s) => write!(f, "--{s}"), } } - - /// 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 { +pub enum QueryError { + MissingNamed(String), + MissingPositional(String), + // String is the debug name of the switch + Repeated(String), } - 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:?}"); + log::fatal!("Invalid name for argument: {name:?}"); } } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0f24c2c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,60 @@ +use switchboard::*; + + +fn main() { + let mut args = Switchboard::from_env(); + args.named("version"); + args.named("verbose").short('v'); + match args.peek() { + Some("run") => { args.pop(); run_program(args) } + Some("asm") => { args.pop(); assemble_program(args) } + _ => run_program(args), + } +} + +fn run_program(mut args: Switchboard) { + println!("RUN PROGRAM"); + + args.positional("source").required(); + args.named("debug").short('d'); + args.named("fullscreen").short('f'); + args.named("zoom").short('z').quick("3").default("1"); + args.raise_errors(); + + let verbose = args.get("verbose").as_bool(); + let version = args.get("version").as_bool(); + let debug = args.get("debug").as_bool(); + let fullscreen = args.get("fullscreen").as_bool(); + let zoom = args.get("zoom").as_u32(); + let source = args.get("source").as_path(); + + println!("Verbose: {verbose:?}"); + println!("Version: {version:?}"); + println!("Source path: {source:?}"); + println!("Debug: {debug:?}"); + println!("Fullscreen: {fullscreen:?}"); + println!("Zoom: {zoom:?}"); +} + +fn assemble_program(mut args: Switchboard) { + println!("ASSEMBLE PROGRAM"); + + args.positional("source"); + args.positional("destination"); + args.positional("extension").default("brc"); + args.named("no-libs"); + args.named("no-project-libs"); + args.raise_errors(); + + let source_path = args.get("source").as_path(); + let destination_path = args.get("destination").as_path(); + let extension = args.get("extension").as_string(); + let no_libs = args.get("no-libs").as_bool(); + let no_project_libs = args.get("no-project-libs").as_bool(); + + println!("Source path: {source_path:?}"); + println!("Destination path: {destination_path:?}"); + println!("Extension: {extension:?}"); + println!("No libs: {no_libs:?}"); + println!("No project libs: {no_project_libs:?}"); +} diff --git a/src/query.rs b/src/query.rs index 4109488..5a9d4ae 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,9 +1,5 @@ use crate::*; -use log::fatal; - -use std::path::PathBuf; - pub struct NamedSwitchQuery<'a> { /// The borrowed switchboard instance. @@ -12,31 +8,99 @@ pub struct NamedSwitchQuery<'a> { 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>, + /// Documentation string. + pub doc: Option<String>, + /// True if this query has already been committed. + pub committed: bool, } impl NamedSwitchQuery<'_> { + pub fn doc(mut self, doc: &str) -> Self { + self.doc = Some(doc.to_string()); + self + } + pub fn short(mut self, short: char) -> Self { if !short.is_ascii_alphanumeric() { - panic!("Invalid short name for argument: {short:?}"); + fatal!("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 } + pub fn optional(&mut self) { + let value = self.get_value(); + self.insert(value); + } + + pub fn default(&mut self, default: &str) { + match self.get_value() { + Some(value) => self.insert(Some(value)), + None => self.insert(Some(Some(default.to_string()))), + }; + } + + pub fn required(&mut self) { + match self.get_value() { + Some(value) => self.insert(Some(value)), + None => { + let error = QueryError::MissingNamed(self.debug_name()); + self.switchboard.errors.push(error); + } + } + } + + fn insert(&mut self, value: Option<Option<String>>) { + let queried = QueriedValue { + doc: self.doc.clone(), + name: self.name.clone(), + variant: QueryVariant::Named(self.debug_name()), + value, + }; + if let Some(_) = self.switchboard.values.insert(self.name.clone(), queried) { + error!("Duplicate query for name {:?}", self.name.clone()); + } + self.committed = true; + } + + fn get_value(&mut self) -> Option<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); + }, + }; + } + // Return a value. + match matches.len() { + 0 => None, + 1 => { + let found = self.switchboard.switches.remove(matches[0]).1; + match found { + Some(string) => Some(Some(string)), + None => Some(self.quick.clone()), + } + } + _ => { + let error = QueryError::Repeated(self.debug_name()); + self.switchboard.errors.push(error); + None + } + } + } + fn debug_name(&self) -> String { let mut debug_name = format!("--{}", self.name); if let Some(short) = self.short { @@ -46,135 +110,73 @@ impl NamedSwitchQuery<'_> { } } +impl Drop for NamedSwitchQuery<'_> { + fn drop(&mut self) { + if !self.committed { + self.optional(); + } + } +} + + 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>, + /// Documentation string. + pub doc: Option<String>, + /// True if this query has already been committed. + pub committed: bool, } impl PositionalSwitchQuery<'_> { - pub fn default(mut self, value: &str) -> Self { - self.default = Some(value.to_string()); + pub fn doc(mut self, doc: &str) -> Self { + self.doc = Some(doc.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}"); + pub fn optional(&mut self) { + let value = self.switchboard.pop(); + self.insert(value); } - fn missing(&self, ty: &str) -> ! { - let name = self.get_name(); - fatal!("The required {name} that takes a {ty} value was not provided"); + pub fn default(&mut self, default: &str) { + match self.switchboard.pop() { + Some(value) => self.insert(Some(value)), + None => self.insert(Some(default.to_string())), + }; } - 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, + pub fn required(&mut self) { + match self.switchboard.pop() { + Some(value) => self.insert(Some(value)), + None => { + let error = QueryError::MissingPositional(self.name.clone()); + self.switchboard.errors.push(error); } - } 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)); - } + fn insert(&mut self, value: Option<String>) { + let queried = QueriedValue { + doc: self.doc.clone(), + name: self.name.clone(), + variant: QueryVariant::Positional(self.switchboard.i), + value: Some(value), + }; + if let Some(_) = self.switchboard.values.insert(self.name.clone(), queried) { + error!("Duplicate query for name {:?}", self.name.clone()); } - return None; + self.committed = true; } - - 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 Drop for PositionalSwitchQuery<'_> { + fn drop(&mut self) { + if !self.committed { + self.optional(); } } } - - -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()) - } -} diff --git a/src/switchboard.rs b/src/switchboard.rs new file mode 100644 index 0000000..f19db13 --- /dev/null +++ b/src/switchboard.rs @@ -0,0 +1,145 @@ +use crate::*; + +use std::collections::{HashMap, VecDeque}; + + +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>>, + // All queried values. + pub values: HashMap<String, QueriedValue>, + // All query errors + pub errors: Vec<QueryError>, + // Total number of popped positional arguments + pub i: usize, +} + +impl Switchboard { + pub fn parse(args: Vec<String>) -> Self { + let mut positional = Vec::new(); + let mut switches = Vec::new(); + let mut unprocessed = None; + let mut args = VecDeque::from(args); + + while let Some(arg) = args.pop_front() { + if arg.is_empty() { + continue; + } else if arg == "--" { + unprocessed = Some(Vec::from(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(); + let values = HashMap::new(); + let errors = Vec::new(); + let i = 0; + Self { + program, positional, switches, unprocessed, values, errors, i + } + } + + 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, + quick: None, + doc: None, + committed: false, + } + } + + pub fn positional(&mut self, name: &str) -> PositionalSwitchQuery { + validate_name(name); + PositionalSwitchQuery { + switchboard: self, + name: name.to_string(), + doc: None, + committed: false, + } + } + + pub fn raise_errors(&self) { + if self.errors.is_empty() + && self.switches.is_empty() + && self.positional.is_empty(){ + return; + } + for (name, _value) in &self.switches { + error!("The switch {name} was not recognised") + } + if !self.positional.is_empty() { + error!("Too many positional arguments provided") + } + for error in &self.errors { + match error { + QueryError::MissingNamed(name) => { + error!("The {name} switch is required") + } + QueryError::MissingPositional(name) => { + error!("The <{name}> argument is required") + } + QueryError::Repeated(name) => { + error!("The {name} switch was passed multiple times") + } + } + } + std::process::exit(1); + } + + pub fn get(&mut self, name: &str) -> QueriedValue { + match self.values.remove(name) { + Some(value) => value, + None => panic!("Name has not been defined: {name:?}"), + } + } + + /// 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) -> Option<String> { + self.i += 1; + self.positional.pop() + } +} + + diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..99468fa --- /dev/null +++ b/src/value.rs @@ -0,0 +1,112 @@ +use crate::*; + +use std::path::PathBuf; + + +macro_rules! as_number { + ($type:ty, $name:expr, $error:ty) => { + paste::paste! { + pub fn [< as_ $type >](&mut self) -> $type { + self.[< as_ $type _opt >]().unwrap_or_else(|| self.missing("number")) + } + pub fn [< as_ $type _opt>](&mut self) -> Option<$type> { + self.value().as_ref().map(|v| v.trim().parse().unwrap_or_else( + |e: $error| self.error(&v, $name, e.to_string()))) + } + } + }; +} + + +pub struct QueriedValue { + pub doc: Option<String>, + pub name: String, + pub variant: QueryVariant, + /// Some if switch passed, then some if value passed. + pub value: Option<Option<String>>, +} + +pub enum QueryVariant { + Positional(usize), + Named(String), +} + +impl QueriedValue { + pub fn value(&self) -> Option<&String> { + match &self.value { + Some(value) => value.as_ref(), + None => None, + } + } + + pub fn as_bool(self) -> bool { + if let Some(value) = self.value { + if let Some(value) = value { + match value.to_lowercase().as_str() { + "y"|"yes"|"t"|"true" => true, + "n"|"no" |"f"|"false" => false, + _ => true, + } + } else { + true + } + } else { + false + } + } + + pub fn as_path(&mut self) -> PathBuf { + self.as_path_opt().unwrap_or_else(|| self.missing("path")) + } + pub fn as_path_opt(&mut self) -> Option<PathBuf> { + if let Some(value) = self.value() { + if !value.is_empty() { + return Some(PathBuf::from(value)); + } + } + return None; + } + + pub fn as_string(&mut self) -> String { + self.as_string_opt().unwrap_or_else(|| self.missing("string")) + } + pub fn as_string_opt(&mut self) -> Option<String> { + self.value().cloned() + } + + 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 } + + + fn locator(&self) -> String { + match &self.variant { + QueryVariant::Positional(_) => { + let name = &self.name; + format!("<{name}> argument") + } + QueryVariant::Named(name) => { + format!("{name} switch") + } + } + } + + fn error(&self, value: &str, ty: &str, err: String) -> ! { + let locator = self.locator(); + fatal!("The value {value:?} passed to the {locator} could not be parsed as a {ty}: {err}"); + } + + fn missing(&self, _ty: &str) -> ! { + let locator = self.locator(); + fatal!("The {locator} is required"); + } +} |