Skip to content

Commit

Permalink
Merge pull request #374 from ozwaldorf/feat/not_strict
Browse files Browse the repository at this point in the history
feat: `ParsePositional::non_strict`
  • Loading branch information
pacak authored Jul 23, 2024
2 parents 74ef468 + cb49fd6 commit fc14443
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 23 deletions.
5 changes: 5 additions & 0 deletions bpaf_derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ impl ToTokens for PostParse {
PostParse::Optional { .. } => quote!(optional()),
PostParse::Parse { f, .. } => quote!(parse(#f)),
PostParse::Strict { .. } => quote!(strict()),
PostParse::NonStrict { .. } => quote!(non_strict()),
PostParse::Anywhere { .. } => quote!(anywhere()),
}
.to_tokens(tokens);
Expand Down Expand Up @@ -256,6 +257,7 @@ pub(crate) enum PostParse {
Optional { span: Span },
Parse { span: Span, f: Box<Expr> },
Strict { span: Span },
NonStrict { span: Span },
Anywhere { span: Span },
}
impl PostParse {
Expand All @@ -271,6 +273,7 @@ impl PostParse {
| Self::Optional { span }
| Self::Parse { span, .. }
| Self::Strict { span }
| Self::NonStrict { span }
| Self::Anywhere { span } => *span,
}
}
Expand Down Expand Up @@ -480,6 +483,8 @@ impl PostParse {
Self::Parse { span, f }
} else if kw == "strict" {
Self::Strict { span }
} else if kw == "non_strict" {
Self::NonStrict { span }
} else if kw == "some" {
let msg = parse_arg(input)?;
Self::Some_ { span, msg }
Expand Down
13 changes: 13 additions & 0 deletions bpaf_derive/src/field_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,19 @@ fn strict_positional_named_fields() {
};
assert_eq!(input.to_token_stream().to_string(), output.to_string());
}

#[test]
fn non_strict_positional_named_fields() {
let input: NamedField = parse_quote! {
#[bpaf(positional("ARG"), non_strict)]
name: String
};
let output = quote! {
::bpaf::positional::<String>("ARG").non_strict()
};
assert_eq!(input.to_token_stream().to_string(), output.to_string());
}

#[test]
fn optional_named_pathed() {
let input: NamedField = parse_quote! {
Expand Down
19 changes: 18 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,14 @@ pub(crate) enum Message {
// those cannot be caught-------------------------------------------------------------
/// Parsing failed and this is the final output
ParseFailure(ParseFailure),

/// Tried to consume a strict positional argument, value was present but was not strictly
/// positional
StrictPos(usize, Metavar),

/// Tried to consume a non-strict positional argument, but the value was strict
NonStrictPos(usize, Metavar),

/// Parser provided by user failed to parse a value
ParseFailed(Option<usize>, String),

Expand Down Expand Up @@ -87,7 +91,8 @@ impl Message {
| Message::ParseSome(_)
| Message::ParseFail(_)
| Message::Missing(_)
| Message::PureFailed(_) => true,
| Message::PureFailed(_)
| Message::NonStrictPos(_, _) => true,
Message::StrictPos(_, _)
| Message::ParseFailed(_, _)
| Message::GuardFailed(_, _)
Expand Down Expand Up @@ -325,6 +330,18 @@ impl Message {
doc.token(Token::BlockEnd(Block::TermRef));
}

// Error: FOO expected to be on the left side of --
Message::NonStrictPos(_ix, metavar) => {
doc.text("expected ");
doc.token(Token::BlockStart(Block::TermRef));
doc.metavar(metavar);
doc.token(Token::BlockEnd(Block::TermRef));
doc.text(" to be on the left side of ");
doc.token(Token::BlockStart(Block::TermRef));
doc.literal("--");
doc.token(Token::BlockEnd(Block::TermRef));
}

// Error: <message from some or fail>
Message::ParseSome(s) | Message::ParseFail(s) => {
doc.text(s);
Expand Down
72 changes: 50 additions & 22 deletions src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -709,21 +709,28 @@ pub(crate) fn build_positional<T>(metavar: &'static str) -> ParsePositional<T> {
ParsePositional {
metavar,
help: None,
result_type: PhantomData,
strict: false,
position: Position::Unrestricted,
ty: PhantomData,
}
}

/// Parse a positional item, created with [`positional`]
/// Parse a positional item, created with [`positional`](crate::positional)
///
/// You can add extra information to positional parsers with [`help`](Self::help)
/// and [`strict`](Self::strict) on this struct.
/// You can add extra information to positional parsers with [`help`](Self::help),
/// [`strict`](Self::strict), or [`non_strict`](Self::non_strict) on this struct.
#[derive(Clone)]
pub struct ParsePositional<T> {
metavar: &'static str,
help: Option<Doc>,
result_type: PhantomData<T>,
strict: bool,
position: Position,
ty: PhantomData<T>,
}

#[derive(Clone, PartialEq, Eq)]
enum Position {
Unrestricted,
Strict,
NonStrict,
}

impl<T> ParsePositional<T> {
Expand Down Expand Up @@ -769,39 +776,59 @@ impl<T> ParsePositional<T> {
/// `bpaf` would display such positional elements differently in usage line as well.
#[cfg_attr(not(doctest), doc = include_str!("docs2/positional_strict.md"))]
#[must_use]
pub fn strict(mut self) -> Self {
self.strict = true;
#[inline(always)]
pub fn strict(mut self) -> ParsePositional<T> {
self.position = Position::Strict;
self
}

/// Changes positional parser to be a "not strict" positional
///
/// Ensures the parser always rejects "strict" positions to the right of the separator, `--`.
/// Essentially the inverse operation to [`ParsePositional::strict`], which can be used to ensure
/// adjacent strict and nonstrict args never conflict with eachother.
#[must_use]
#[inline(always)]
pub fn non_strict(mut self) -> Self {
self.position = Position::NonStrict;
self
}

#[inline(always)]
fn meta(&self) -> Meta {
let meta = Meta::from(Item::Positional {
metavar: Metavar(self.metavar),
help: self.help.clone(),
});
if self.strict {
Meta::Strict(Box::new(meta))
} else {
meta
match self.position {
Position::Strict => Meta::Strict(Box::new(meta)),
_ => meta,
}
}
}

fn parse_pos_word(
args: &mut State,
strict: bool,
metavar: &'static str,
metavar: Metavar,
help: &Option<Doc>,
position: &Position,
) -> Result<OsString, Error> {
let metavar = Metavar(metavar);
match args.take_positional_word(metavar) {
Ok((ix, is_strict, word)) => {
if strict && !is_strict {
#[cfg(feature = "autocomplete")]
args.push_pos_sep();

return Err(Error(Message::StrictPos(ix, metavar)));
match position {
&Position::Strict if !is_strict => {
#[cfg(feature = "autocomplete")]
args.push_pos_sep();
return Err(Error(Message::StrictPos(ix, metavar)));
}
&Position::NonStrict if is_strict => {
#[cfg(feature = "autocomplete")]
args.push_pos_sep();
return Err(Error(Message::NonStrictPos(ix, metavar)));
}
_ => {}
}

#[cfg(feature = "autocomplete")]
if args.touching_last_remove() && !args.check_no_pos_ahead() {
args.push_metavar(metavar.0, help, false);
Expand All @@ -826,13 +853,14 @@ where
<T as std::str::FromStr>::Err: std::fmt::Display,
{
fn eval(&self, args: &mut State) -> Result<T, Error> {
let os = parse_pos_word(args, self.strict, self.metavar, &self.help)?;
let os = parse_pos_word(args, Metavar(self.metavar), &self.help, &self.position)?;
match parse_os_str::<T>(os) {
Ok(ok) => Ok(ok),
Err(err) => Err(Error(Message::ParseFailed(args.current, err))),
}
}

#[inline(always)]
fn meta(&self) -> Meta {
self.meta()
}
Expand Down
14 changes: 14 additions & 0 deletions tests/positionals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,17 @@ fn strictly_positional() {
let r = parser.run_inner(&["--"]).unwrap_err().unwrap_stderr();
assert_eq!(r, "expected `A`, pass `--help` for usage information");
}

#[test]
fn non_strictly_positional() {
let parser = positional::<String>("A").non_strict().to_options();

let r = parser.run_inner(&["a"]).unwrap();
assert_eq!(r, "a");

let r = parser.run_inner(&["--", "a"]).unwrap_err().unwrap_stderr();
assert_eq!(r, "expected `A` to be on the left side of `--`");

let r = parser.run_inner(&["--"]).unwrap_err().unwrap_stderr();
assert_eq!(r, "expected `A`, pass `--help` for usage information");
}

0 comments on commit fc14443

Please sign in to comment.