Skip to content

Commit

Permalink
Merge #127: Argument parser mode for rejecting long options
Browse files Browse the repository at this point in the history
  • Loading branch information
magicant committed Jan 4, 2022
2 parents ee23326 + 605130a commit 19e6763
Showing 1 changed file with 111 additions and 27 deletions.
138 changes: 111 additions & 27 deletions yash-builtin/src/common/arg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
//! ];
//!
//! let arguments = Field::dummies(["foo", "-ba", "--baz", "--", "--bar", "--", "-a", "foo"]);
//! let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
//! let (options, operands) = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
//! assert_eq!(options.len(), 4);
//! assert_eq!(options[0].spec, &specs[1]); // 'b' in "-ba"
//! assert_eq!(options[0].argument, None);
Expand Down Expand Up @@ -183,9 +183,53 @@ impl OptionSpec<'_> {
}
}

/// TODO
/// Configuration for customizing the argument parsing behavior
///
/// # Examples
///
/// The default configuration disables all non-portable extensions:
///
/// ```
/// # use yash_builtin::common::arg::Mode;
/// let mode = Mode::default();
/// assert!(!mode.accepts_long_options());
/// # // TODO other properties
/// ```
///
/// The [`with_extensions`](Self::with_extensions) function returns a `Mode`
/// with those extensions enabled.
///
/// ```
/// # use yash_builtin::common::arg::Mode;
/// let mode = Mode::with_extensions();
/// assert!(mode.accepts_long_options());
/// # // TODO other properties
/// ```
#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
pub struct Mode {}
pub struct Mode {
long_options: bool,
// TODO options_after_operands
// TODO negative_integer_operands
// TODO rejecting_non_portable_option_specs
}

impl Mode {
/// Returns a new `Mode` with non-portable extensions enabled.
pub const fn with_extensions() -> Self {
Mode { long_options: true }
}

/// Whether the parser accepts long options or not
pub const fn accepts_long_options(&self) -> bool {
self.long_options
}

/// Sets whether the parser accepts long options or not.
pub fn accept_long_options(&mut self, accept: bool) -> &mut Self {
self.long_options = accept;
self
}
}

/// Occurrence of an option
#[derive(Clone, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -214,6 +258,10 @@ pub enum Error<'a> {
/// Long option that is not defined in the option specs
UnknownLongOption(Field),

/// Long option that is defined in an option spec but disabled by
/// configuration ([`Mode`]).
UnsupportedLongOption(Field, &'a OptionSpec<'a>),

/// Long option that matches more than one option spec
///
/// The second item of the tuple is a list of all option specs that matched.
Expand All @@ -233,6 +281,7 @@ impl Error<'_> {
match self {
UnknownShortOption(_char, field) => field,
UnknownLongOption(field) => field,
UnsupportedLongOption(field, _spec) => field,
AmbiguousLongOption(field, _specs) => field,
MissingOptionArgument(field, _spec) => field,
UnexpectedOptionArgument(field, _spec) => field,
Expand All @@ -255,6 +304,9 @@ impl std::fmt::Display for Error<'_> {
UnknownLongOption(field) => {
write!(f, "unknown option {:?}", long_option_name(field))
}
UnsupportedLongOption(field, _spec) => {
write!(f, "unsupported option {:?}", field.value)
}
AmbiguousLongOption(field, _specs) => {
write!(f, "ambiguous option {:?}", long_option_name(field))
}
Expand Down Expand Up @@ -381,6 +433,7 @@ fn long_match<'a>(
/// the argument.
fn parse_long_option<'a, I: Iterator<Item = Field>>(
option_specs: &'a [OptionSpec<'a>],
mode: Mode,
arguments: &mut Peekable<I>,
) -> Result<Option<OptionOccurrence<'a>>, Error<'a>> {
fn starts_with_double_hyphen(field: &Field) -> bool {
Expand All @@ -403,7 +456,8 @@ fn parse_long_option<'a, I: Iterator<Item = Field>>(
};

let spec = match long_match(option_specs, name) {
Ok(spec) => spec,
Ok(spec) if mode.accepts_long_options() => spec,
Ok(spec) => return Err(Error::UnsupportedLongOption(field, spec)),
Err(matched_specs) => {
return Err(if matched_specs.is_empty() {
Error::UnknownLongOption(field)
Expand Down Expand Up @@ -442,7 +496,7 @@ fn parse_long_option<'a, I: Iterator<Item = Field>>(
/// If successful, returns a pair of option occurrences and operands.
pub fn parse_arguments<'a>(
option_specs: &'a [OptionSpec<'a>],
_mode: Mode,
mode: Mode,
arguments: Vec<Field>,
) -> Result<(Vec<OptionOccurrence<'a>>, Vec<Field>), Error<'a>> {
let mut arguments = arguments.into_iter().skip(1).peekable();
Expand All @@ -452,7 +506,7 @@ pub fn parse_arguments<'a>(
if parse_short_options(option_specs, &mut arguments, &mut option_occurrences)? {
continue;
}
if let Some(occurrence) = parse_long_option(option_specs, &mut arguments)? {
if let Some(occurrence) = parse_long_option(option_specs, mode, &mut arguments)? {
option_occurrences.push(occurrence);
continue;
}
Expand Down Expand Up @@ -756,12 +810,14 @@ mod tests {
let specs = &[OptionSpec::new().long("option")];

let arguments = Field::dummies(["foo"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options, []);
assert_eq!(operands, []);

let arguments = Field::dummies(["foo", ""]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options, []);
assert_eq!(operands, Field::dummies([""]));
}
Expand All @@ -771,13 +827,15 @@ mod tests {
let specs = &[OptionSpec::new().long("option")];

let arguments = Field::dummies(["foo", "--option"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 1, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("option"));
assert_eq!(operands, []);

let arguments = Field::dummies(["foo", "--option", "bar"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 1, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("option"));
assert_eq!(operands, Field::dummies(["bar"]));
Expand All @@ -788,14 +846,16 @@ mod tests {
let specs = &[OptionSpec::new().long("foo")];

let arguments = Field::dummies(["command", "--foo", "--foo"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 2, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("foo"));
assert_eq!(options[1].spec.get_long(), Some("foo"));
assert_eq!(operands, []);

let arguments = Field::dummies(["command", "--foo", "--foo", "argument"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 2, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("foo"));
assert_eq!(options[1].spec.get_long(), Some("foo"));
Expand All @@ -807,14 +867,16 @@ mod tests {
let specs = &[OptionSpec::new().long("foo"), OptionSpec::new().long("bar")];

let arguments = Field::dummies(["command", "--foo", "--bar", "!"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 2, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("foo"));
assert_eq!(options[1].spec.get_long(), Some("bar"));
assert_eq!(operands, Field::dummies(["!"]));

let arguments = Field::dummies(["command", "--bar", "--foo", "--bar"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 3, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("bar"));
assert_eq!(options[1].spec.get_long(), Some("foo"));
Expand All @@ -827,7 +889,8 @@ mod tests {
let specs = &[OptionSpec::new().long("min")];

let arguments = Field::dummies(["command", "--mi"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 1, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("min"));
assert_eq!(operands, []);
Expand All @@ -838,7 +901,8 @@ mod tests {
let specs = &[OptionSpec::new().long("max"), OptionSpec::new().long("min")];

let arguments = Field::dummies(["command", "--mi"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 1, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("min"));
assert_eq!(operands, []);
Expand All @@ -853,7 +917,8 @@ mod tests {
];

let arguments = Field::dummies(["command", "--man"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 1, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("man"));
assert_eq!(operands, []);
Expand All @@ -866,7 +931,8 @@ mod tests {
.argument(OptionArgumentSpec::Required)];

let arguments = Field::dummies(["foo", "--option="]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 1, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("option"));
assert_matches!(options[0].argument, Some(ref field) => {
Expand All @@ -876,7 +942,8 @@ mod tests {
assert_eq!(operands, []);

let arguments = Field::dummies(["foo", "--option=x", "--option=value", "argument"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 2, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("option"));
assert_matches!(options[0].argument, Some(ref field) => {
Expand All @@ -898,7 +965,8 @@ mod tests {
.argument(OptionArgumentSpec::Required)];

let arguments = Field::dummies(["foo", "--option", ""]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 1, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("option"));
assert_matches!(options[0].argument, Some(ref field) => {
Expand All @@ -908,7 +976,8 @@ mod tests {
assert_eq!(operands, []);

let arguments = Field::dummies(["foo", "--option", "x", "--option", "value", "argument"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 2, "{:?}", options);
assert_eq!(options[0].spec.get_long(), Some("option"));
assert_matches!(options[0].argument, Some(ref field) => {
Expand All @@ -930,7 +999,8 @@ mod tests {
.argument(OptionArgumentSpec::Required)];

let arguments = Field::dummies(["foo", "-a", "argument", "-a", "--", "--", "operand"]);
let (options, operands) = parse_arguments(specs, Mode::default(), arguments).unwrap();
let (options, operands) =
parse_arguments(specs, Mode::with_extensions(), arguments).unwrap();
assert_eq!(options.len(), 2, "{:?}", options);
assert_eq!(options[0].spec.get_short(), Some('a'));
assert_matches!(options[0].argument, Some(ref field) => {
Expand Down Expand Up @@ -973,20 +1043,34 @@ mod tests {
let specs = &[OptionSpec::new().long("one")];

let arguments = Field::dummies(["foo", "--two"]);
let error = parse_arguments(&[], Mode::default(), arguments).unwrap_err();
let error = parse_arguments(&[], Mode::with_extensions(), arguments).unwrap_err();
assert_matches!(&error, Error::UnknownLongOption(field) => {
assert_eq!(field.value, "--two");
});
assert_eq!(error.to_string(), "unknown option \"--two\"");

let arguments = Field::dummies(["foo", "--two=three"]);
let error = parse_arguments(specs, Mode::default(), arguments).unwrap_err();
let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
assert_matches!(&error, Error::UnknownLongOption(field) => {
assert_eq!(field.value, "--two=three");
});
assert_eq!(error.to_string(), "unknown option \"--two\"");
}

#[test]
fn disabled_long_option() {
let specs = &[OptionSpec::new().long("option")];

let mode = *Mode::with_extensions().accept_long_options(false);
let arguments = Field::dummies(["foo", "--option"]);
let error = parse_arguments(specs, mode, arguments).unwrap_err();
assert_matches!(&error, &Error::UnsupportedLongOption(ref field, spec) => {
assert_eq!(field.value, "--option");
assert_eq!(spec, &specs[0]);
});
assert_eq!(error.to_string(), "unsupported option \"--option\"");
}

#[test]
fn ambiguous_long_option() {
let specs = &[
Expand All @@ -996,7 +1080,7 @@ mod tests {
];

let arguments = Field::dummies(["command", "--m"]);
let error = parse_arguments(specs, Mode::default(), arguments).unwrap_err();
let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
assert_matches!(&error, Error::AmbiguousLongOption(field, matched_specs) => {
assert_eq!(field.value, "--m");
assert_eq!(matched_specs.as_slice(), [&specs[0], &specs[1]]);
Expand Down Expand Up @@ -1038,7 +1122,7 @@ mod tests {
];

let arguments = Field::dummies(["command", "--fo"]);
let error = parse_arguments(specs, Mode::default(), arguments).unwrap_err();
let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
assert_matches!(&error, &Error::MissingOptionArgument(ref field, spec) => {
assert_eq!(field.value, "--fo");
assert_eq!(spec, &specs[0]);
Expand All @@ -1055,7 +1139,7 @@ mod tests {
];

let arguments = Field::dummies(["command", "--bar=baz"]);
let error = parse_arguments(specs, Mode::default(), arguments).unwrap_err();
let error = parse_arguments(specs, Mode::with_extensions(), arguments).unwrap_err();
assert_matches!(&error, &Error::UnexpectedOptionArgument(ref field, spec) => {
assert_eq!(field.value, "--bar=baz");
assert_eq!(spec, &specs[1]);
Expand Down

0 comments on commit 19e6763

Please sign in to comment.