diff --git a/src/lib.rs b/src/lib.rs index 55c47fc..1806ddb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,15 @@ use std::collections::{HashMap, HashSet}; use std::env::args_os; +use std::ffi::{OsStr, OsString}; use std::ops::RangeBounds; /// A command line argument parser. #[derive(Debug, Clone)] pub struct Parser { - flags: Vec, args: Vec, - pos_args: Vec, // Used to ensure we don't get duplicate arguments. + id_set: HashSet, long_set: HashSet, short_set: HashSet, } @@ -17,136 +17,223 @@ pub struct Parser { impl Parser { pub fn new() -> Parser { Parser { - flags: Vec::new(), args: Vec::new(), - pos_args: Vec::new(), + id_set: HashSet::new(), long_set: HashSet::new(), short_set: HashSet::new(), } } - /// Add a flag argument. - pub fn flag(&mut self, long: &str, short: Option<&str>, doc_string: &str) { - self.ensure_not_duplicate(long, short); + /// Add a flag (bool) argument. + /// + /// - `id`: the argument identifier, used for fetching argument + /// matches. + /// - `flags`: the long and/or short argument flag strings. Must be + /// in the form "-f" or "--flag". You can pass as many as you + /// like, all of which will be considered equivalent during + /// parsing. But there must be at least one. + /// - `doc`: the documentation string to use in the generated help. + /// Pass an empty string to indicate no documentation. + pub fn add_flag(&mut self, id: &str, flags: &[&str], doc: &str) { + let (long_flags, short_flags) = self.validate_and_process_arg(id, flags); - self.flags.push(Flag { - long: long.into(), - short: short.map(|s| s.into()), - doc_string: doc_string.into(), + self.args.push(Arg { + arg_type: ArgType::Flag, + id: id.into(), + value_label: String::new(), + long_flags: long_flags, + short_flags: short_flags, + acceptable_count: (0, None), + doc: doc.into(), }); } - /// Add a standard argument. - pub fn argument>( + /// Add a standard argument, that takes a value. + pub fn add_argument( &mut self, - long: &str, - short: Option<&str>, - doc_string: &str, - value_name: &str, - acceptable_count: R, + id: &str, + flags: &[&str], + doc: &str, + value_label: &str, + required: bool, ) { - self.ensure_not_duplicate(long, short); - - use std::ops::Bound; - let count_start: Option = match acceptable_count.start_bound() { - Bound::Included(&n) => Some(n), - Bound::Excluded(&n) => Some(n + 1), - Bound::Unbounded => None, - }; - let count_end: Option = match acceptable_count.end_bound() { - Bound::Included(&n) => Some(n + 1), - Bound::Excluded(&n) => Some(n), - Bound::Unbounded => None, - }; + let (long_flags, short_flags) = self.validate_and_process_arg(id, flags); self.args.push(Arg { - long: long.into(), - short: short.map(|s| s.into()), - acceptable_count: (count_start, count_end), - value_name: value_name.into(), - doc_string: doc_string.into(), + arg_type: ArgType::Arg, + id: id.into(), + value_label: value_label.into(), + long_flags: long_flags, + short_flags: short_flags, + acceptable_count: (if required { 1 } else { 0 }, None), + doc: doc.into(), }); } /// Add a positional argument. /// - /// There can only be one positional argument with `count == None`, - /// which represents any left over positional arguments after those - /// with explicit counts have been parsed. - pub fn positional_argument( + /// Unlike flags and standard arguments, positional arguments are + /// parsed in the order they're added. Because of their nature, + /// they have some additional considerations: + /// + /// - All required positional arguments must precede all optional + /// positional arguments. + /// - There can at most be a single positional multi-argument, + /// which must come last. (See `add_positional_multi_argument()`.) + pub fn add_positional_argument( &mut self, - doc_string: &str, - value_name: &str, - count: Option, + id: &str, + doc: &str, + value_label: &str, + required: bool, ) { - self.pos_args.push(PosArg { - count: count, - value_name: value_name.into(), - doc_string: doc_string.into(), + let (_, _) = self.validate_and_process_arg(id, &[]); + + self.args.push(Arg { + arg_type: ArgType::PosArg, + id: id.into(), + value_label: value_label.into(), + long_flags: Vec::new(), + short_flags: Vec::new(), + acceptable_count: (if required { 1 } else { 0 }, Some(1)), + doc: doc.into(), + }); + } + + pub fn add_positional_multi_argument( + &mut self, + id: &str, + doc: &str, + value_label: &str, + required: bool, + ) { + let (_, _) = self.validate_and_process_arg(id, &[]); + + self.args.push(Arg { + arg_type: ArgType::PosArg, + id: id.into(), + value_label: value_label.into(), + long_flags: Vec::new(), + short_flags: Vec::new(), + acceptable_count: (if required { 1 } else { 0 }, None), + doc: doc.into(), }); } //---------------- - fn ensure_not_duplicate(&mut self, long: &str, short: Option<&str>) { - if self.long_set.contains(long) { - panic!("Attempted to add duplicate long argument \"--{}\".", long); - } - self.long_set.insert(long.into()); + pub fn parse(self) -> ParsedArguments { + todo!() + } - if let Some(short) = short { - if self.short_set.contains(short) { - panic!("Attempted to add duplicate short argument \"-{}\".", short); - } - self.short_set.insert(short.into()); + //---------------- + + /// Returns (long, short) pair, each of which is a Vec of argument strings with + /// the leading hyphens stripped off. + fn validate_and_process_arg(&mut self, id: &str, flags: &[&str]) -> (Vec, Vec) { + if self.id_set.contains(id) { + panic!( + "Error: attempted to add argument with a duplicate ID \"{}\".", + id + ); } + self.id_set.insert(id.into()); + + let mut long_flags = Vec::new(); + let mut short_flags = Vec::new(); + + for &flag in flags { + // Ensure no whitespace. + if flag.len() != flag.trim().len() || flag.split_whitespace().count() > 1 { + panic!( + "Error: attempted to add argument \"{}\" which contains whitespace.", + flag + ); + } + // Long flags. + else if flag.starts_with("--") && flag.len() > 2 { + if self.long_set.contains(flag) { + panic!( + "Error: attempted to add duplicate long argument \"{}\".", + flag + ); + } + self.long_set.insert(flag.into()); + long_flags.push((&flag[2..]).into()); + } + // Check if it's a valid short flag (should only have one character + // after the hyphen). + else if flag.starts_with("-") && flag.chars().count() == 2 { + if self.short_set.contains(flag) { + panic!( + "Error: attempted to add duplicate short argument \"{}\".", + flag + ); + } + self.short_set.insert(flag.into()); + short_flags.push((&flag[1..]).into()); + } + // Not a valid flag. + else { + panic!( + "Error: attempted to add argument \"{}\", which isn't a valid argument string.", + flag + ) + } + } + + (long_flags, short_flags) } } /// Parsed command line arguments. #[derive(Debug, Clone)] -pub struct Arguments { - // TODO: allow OsString argument values. - /// The flags that were passed, indexed by `long`. - pub flags: HashSet, +pub struct ParsedArguments { + // A list of all passed arguments, in the order they were + // passed. Each item is a `(argument_id, argument_value)` tuple. + // Boolean flags do not have a value. + arguments: Vec<(String, Option)>, - /// Non-positional arguments that were passed, indexed by `long`. - pub args: HashMap>, - - /// Positional arguments, indexed by `value_name`. - pub positional: HashMap>, + // Maps from an argument's ID to the indices in `arguments` where it + // is stored. + id_map: HashMap>, // Argument ID -> index list } //------------------------------------------------------------- -/// Flag spec. -#[derive(Debug, Clone)] -struct Flag { - long: String, - short: Option, - - // For documentation. - doc_string: String, +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +enum ArgType { + Flag, + Arg, + PosArg, } -/// Argument spec. +/// Argument specification. #[derive(Debug, Clone)] struct Arg { - long: String, - short: Option, - acceptable_count: (Option, Option), + arg_type: ArgType, + id: String, + value_label: String, - // For documentation. - value_name: String, - doc_string: String, -} - -/// Positional argument spec. -#[derive(Debug, Clone)] -struct PosArg { - count: Option, - - // For documentation. - value_name: String, - doc_string: String, + // Long and short versions of the argument flag. E.g. "--curve" and + // "-c", but without the leading dashes. + long_flags: Vec, + short_flags: Vec, + + // How many instances of the argument can be present, specified + // as a range. + // + // For example: + // - (0, None): An argument that can show up any number of times, + // including not at all. + // - (0, 1): An argument that can either be absent or show up + // precisely once. + // - (1, 1): An argument that must show up precisely once. + // - (1, None): An argument that must show up at least once. + // - (2, 9): An argument that must show up at least twice, but + // no more than 9 times. + acceptable_count: (usize, Option), + + // Documentation string, for generated help. + doc: String, }