Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Argument parser mode for rejecting long options #127

Merged
merged 4 commits into from
Jan 4, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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