summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib.rs112
-rw-r--r--src/main.rs60
-rw-r--r--src/query.rs236
-rw-r--r--src/switchboard.rs145
-rw-r--r--src/value.rs112
5 files changed, 454 insertions, 211 deletions
diff --git a/src/lib.rs b/src/lib.rs
index e25725d..d968cf2 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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");
+ }
+}