Somewhat improved structure for how things work.
This commit is contained in:
parent
db277c7805
commit
0fb23efa1f
269
src/lib.rs
269
src/lib.rs
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user