Somewhat improved structure for how things work.

This commit is contained in:
Nathan Vegdahl 2022-06-14 21:32:49 -07:00
parent db277c7805
commit 0fb23efa1f

View File

@ -1,15 +1,15 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env::args_os; use std::env::args_os;
use std::ffi::{OsStr, OsString};
use std::ops::RangeBounds; use std::ops::RangeBounds;
/// A command line argument parser. /// A command line argument parser.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Parser { pub struct Parser {
flags: Vec<Flag>,
args: Vec<Arg>, args: Vec<Arg>,
pos_args: Vec<PosArg>,
// Used to ensure we don't get duplicate arguments. // Used to ensure we don't get duplicate arguments.
id_set: HashSet<String>,
long_set: HashSet<String>, long_set: HashSet<String>,
short_set: HashSet<String>, short_set: HashSet<String>,
} }
@ -17,136 +17,223 @@ pub struct Parser {
impl Parser { impl Parser {
pub fn new() -> Parser { pub fn new() -> Parser {
Parser { Parser {
flags: Vec::new(),
args: Vec::new(), args: Vec::new(),
pos_args: Vec::new(), id_set: HashSet::new(),
long_set: HashSet::new(), long_set: HashSet::new(),
short_set: HashSet::new(), short_set: HashSet::new(),
} }
} }
/// Add a flag argument. /// Add a flag (bool) argument.
pub fn flag(&mut self, long: &str, short: Option<&str>, doc_string: &str) { ///
self.ensure_not_duplicate(long, short); /// - `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 { self.args.push(Arg {
long: long.into(), arg_type: ArgType::Flag,
short: short.map(|s| s.into()), id: id.into(),
doc_string: doc_string.into(), value_label: String::new(),
long_flags: long_flags,
short_flags: short_flags,
acceptable_count: (0, None),
doc: doc.into(),
}); });
} }
/// Add a standard argument. /// Add a standard argument, that takes a value.
pub fn argument<R: RangeBounds<usize>>( pub fn add_argument(
&mut self, &mut self,
long: &str, id: &str,
short: Option<&str>, flags: &[&str],
doc_string: &str, doc: &str,
value_name: &str, value_label: &str,
acceptable_count: R, required: bool,
) { ) {
self.ensure_not_duplicate(long, short); let (long_flags, short_flags) = self.validate_and_process_arg(id, flags);
use std::ops::Bound;
let count_start: Option<usize> = match acceptable_count.start_bound() {
Bound::Included(&n) => Some(n),
Bound::Excluded(&n) => Some(n + 1),
Bound::Unbounded => None,
};
let count_end: Option<usize> = match acceptable_count.end_bound() {
Bound::Included(&n) => Some(n + 1),
Bound::Excluded(&n) => Some(n),
Bound::Unbounded => None,
};
self.args.push(Arg { self.args.push(Arg {
long: long.into(), arg_type: ArgType::Arg,
short: short.map(|s| s.into()), id: id.into(),
acceptable_count: (count_start, count_end), value_label: value_label.into(),
value_name: value_name.into(), long_flags: long_flags,
doc_string: doc_string.into(), short_flags: short_flags,
acceptable_count: (if required { 1 } else { 0 }, None),
doc: doc.into(),
}); });
} }
/// Add a positional argument. /// Add a positional argument.
/// ///
/// There can only be one positional argument with `count == None`, /// Unlike flags and standard arguments, positional arguments are
/// which represents any left over positional arguments after those /// parsed in the order they're added. Because of their nature,
/// with explicit counts have been parsed. /// they have some additional considerations:
pub fn positional_argument( ///
/// - 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, &mut self,
doc_string: &str, id: &str,
value_name: &str, doc: &str,
count: Option<usize>, value_label: &str,
required: bool,
) { ) {
self.pos_args.push(PosArg { let (_, _) = self.validate_and_process_arg(id, &[]);
count: count,
value_name: value_name.into(), self.args.push(Arg {
doc_string: doc_string.into(), 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>) { pub fn parse(self) -> ParsedArguments {
if self.long_set.contains(long) { todo!()
panic!("Attempted to add duplicate long argument \"--{}\".", long); }
}
self.long_set.insert(long.into());
if let Some(short) = short { //----------------
if self.short_set.contains(short) {
panic!("Attempted to add duplicate short argument \"-{}\".", short); /// Returns (long, short) pair, each of which is a Vec of argument strings with
} /// the leading hyphens stripped off.
self.short_set.insert(short.into()); fn validate_and_process_arg(&mut self, id: &str, flags: &[&str]) -> (Vec<String>, Vec<String>) {
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. /// Parsed command line arguments.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Arguments { pub struct ParsedArguments {
// TODO: allow OsString argument values. // A list of all passed arguments, in the order they were
/// The flags that were passed, indexed by `long`. // passed. Each item is a `(argument_id, argument_value)` tuple.
pub flags: HashSet<String>, // Boolean flags do not have a value.
arguments: Vec<(String, Option<OsString>)>,
/// Non-positional arguments that were passed, indexed by `long`. // Maps from an argument's ID to the indices in `arguments` where it
pub args: HashMap<String, Vec<String>>, // is stored.
id_map: HashMap<String, Vec<usize>>, // Argument ID -> index list
/// Positional arguments, indexed by `value_name`.
pub positional: HashMap<String, Vec<String>>,
} }
//------------------------------------------------------------- //-------------------------------------------------------------
/// Flag spec. #[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[derive(Debug, Clone)] enum ArgType {
struct Flag { Flag,
long: String, Arg,
short: Option<String>, PosArg,
// For documentation.
doc_string: String,
} }
/// Argument spec. /// Argument specification.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Arg { struct Arg {
long: String, arg_type: ArgType,
short: Option<String>, id: String,
acceptable_count: (Option<usize>, Option<usize>), value_label: String,
// For documentation. // Long and short versions of the argument flag. E.g. "--curve" and
value_name: String, // "-c", but without the leading dashes.
doc_string: String, long_flags: Vec<String>,
} short_flags: Vec<String>,
/// Positional argument spec. // How many instances of the argument can be present, specified
#[derive(Debug, Clone)] // as a range.
struct PosArg { //
count: Option<usize>, // For example:
// - (0, None): An argument that can show up any number of times,
// For documentation. // including not at all.
value_name: String, // - (0, 1): An argument that can either be absent or show up
doc_string: String, // 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<usize>),
// Documentation string, for generated help.
doc: String,
} }