Skip to content

Commit fc14443

Browse files
authored
Merge pull request #374 from ozwaldorf/feat/not_strict
feat: `ParsePositional::non_strict`
2 parents 74ef468 + cb49fd6 commit fc14443

File tree

5 files changed

+100
-23
lines changed

5 files changed

+100
-23
lines changed

bpaf_derive/src/attrs.rs

+5
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ impl ToTokens for PostParse {
217217
PostParse::Optional { .. } => quote!(optional()),
218218
PostParse::Parse { f, .. } => quote!(parse(#f)),
219219
PostParse::Strict { .. } => quote!(strict()),
220+
PostParse::NonStrict { .. } => quote!(non_strict()),
220221
PostParse::Anywhere { .. } => quote!(anywhere()),
221222
}
222223
.to_tokens(tokens);
@@ -256,6 +257,7 @@ pub(crate) enum PostParse {
256257
Optional { span: Span },
257258
Parse { span: Span, f: Box<Expr> },
258259
Strict { span: Span },
260+
NonStrict { span: Span },
259261
Anywhere { span: Span },
260262
}
261263
impl PostParse {
@@ -271,6 +273,7 @@ impl PostParse {
271273
| Self::Optional { span }
272274
| Self::Parse { span, .. }
273275
| Self::Strict { span }
276+
| Self::NonStrict { span }
274277
| Self::Anywhere { span } => *span,
275278
}
276279
}
@@ -480,6 +483,8 @@ impl PostParse {
480483
Self::Parse { span, f }
481484
} else if kw == "strict" {
482485
Self::Strict { span }
486+
} else if kw == "non_strict" {
487+
Self::NonStrict { span }
483488
} else if kw == "some" {
484489
let msg = parse_arg(input)?;
485490
Self::Some_ { span, msg }

bpaf_derive/src/field_tests.rs

+13
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,19 @@ fn strict_positional_named_fields() {
485485
};
486486
assert_eq!(input.to_token_stream().to_string(), output.to_string());
487487
}
488+
489+
#[test]
490+
fn non_strict_positional_named_fields() {
491+
let input: NamedField = parse_quote! {
492+
#[bpaf(positional("ARG"), non_strict)]
493+
name: String
494+
};
495+
let output = quote! {
496+
::bpaf::positional::<String>("ARG").non_strict()
497+
};
498+
assert_eq!(input.to_token_stream().to_string(), output.to_string());
499+
}
500+
488501
#[test]
489502
fn optional_named_pathed() {
490503
let input: NamedField = parse_quote! {

src/error.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ pub(crate) enum Message {
4242
// those cannot be caught-------------------------------------------------------------
4343
/// Parsing failed and this is the final output
4444
ParseFailure(ParseFailure),
45+
4546
/// Tried to consume a strict positional argument, value was present but was not strictly
4647
/// positional
4748
StrictPos(usize, Metavar),
4849

50+
/// Tried to consume a non-strict positional argument, but the value was strict
51+
NonStrictPos(usize, Metavar),
52+
4953
/// Parser provided by user failed to parse a value
5054
ParseFailed(Option<usize>, String),
5155

@@ -87,7 +91,8 @@ impl Message {
8791
| Message::ParseSome(_)
8892
| Message::ParseFail(_)
8993
| Message::Missing(_)
90-
| Message::PureFailed(_) => true,
94+
| Message::PureFailed(_)
95+
| Message::NonStrictPos(_, _) => true,
9196
Message::StrictPos(_, _)
9297
| Message::ParseFailed(_, _)
9398
| Message::GuardFailed(_, _)
@@ -325,6 +330,18 @@ impl Message {
325330
doc.token(Token::BlockEnd(Block::TermRef));
326331
}
327332

333+
// Error: FOO expected to be on the left side of --
334+
Message::NonStrictPos(_ix, metavar) => {
335+
doc.text("expected ");
336+
doc.token(Token::BlockStart(Block::TermRef));
337+
doc.metavar(metavar);
338+
doc.token(Token::BlockEnd(Block::TermRef));
339+
doc.text(" to be on the left side of ");
340+
doc.token(Token::BlockStart(Block::TermRef));
341+
doc.literal("--");
342+
doc.token(Token::BlockEnd(Block::TermRef));
343+
}
344+
328345
// Error: <message from some or fail>
329346
Message::ParseSome(s) | Message::ParseFail(s) => {
330347
doc.text(s);

src/params.rs

+50-22
Original file line numberDiff line numberDiff line change
@@ -709,21 +709,28 @@ pub(crate) fn build_positional<T>(metavar: &'static str) -> ParsePositional<T> {
709709
ParsePositional {
710710
metavar,
711711
help: None,
712-
result_type: PhantomData,
713-
strict: false,
712+
position: Position::Unrestricted,
713+
ty: PhantomData,
714714
}
715715
}
716716

717-
/// Parse a positional item, created with [`positional`]
717+
/// Parse a positional item, created with [`positional`](crate::positional)
718718
///
719-
/// You can add extra information to positional parsers with [`help`](Self::help)
720-
/// and [`strict`](Self::strict) on this struct.
719+
/// You can add extra information to positional parsers with [`help`](Self::help),
720+
/// [`strict`](Self::strict), or [`non_strict`](Self::non_strict) on this struct.
721721
#[derive(Clone)]
722722
pub struct ParsePositional<T> {
723723
metavar: &'static str,
724724
help: Option<Doc>,
725-
result_type: PhantomData<T>,
726-
strict: bool,
725+
position: Position,
726+
ty: PhantomData<T>,
727+
}
728+
729+
#[derive(Clone, PartialEq, Eq)]
730+
enum Position {
731+
Unrestricted,
732+
Strict,
733+
NonStrict,
727734
}
728735

729736
impl<T> ParsePositional<T> {
@@ -769,39 +776,59 @@ impl<T> ParsePositional<T> {
769776
/// `bpaf` would display such positional elements differently in usage line as well.
770777
#[cfg_attr(not(doctest), doc = include_str!("docs2/positional_strict.md"))]
771778
#[must_use]
772-
pub fn strict(mut self) -> Self {
773-
self.strict = true;
779+
#[inline(always)]
780+
pub fn strict(mut self) -> ParsePositional<T> {
781+
self.position = Position::Strict;
782+
self
783+
}
784+
785+
/// Changes positional parser to be a "not strict" positional
786+
///
787+
/// Ensures the parser always rejects "strict" positions to the right of the separator, `--`.
788+
/// Essentially the inverse operation to [`ParsePositional::strict`], which can be used to ensure
789+
/// adjacent strict and nonstrict args never conflict with eachother.
790+
#[must_use]
791+
#[inline(always)]
792+
pub fn non_strict(mut self) -> Self {
793+
self.position = Position::NonStrict;
774794
self
775795
}
776796

797+
#[inline(always)]
777798
fn meta(&self) -> Meta {
778799
let meta = Meta::from(Item::Positional {
779800
metavar: Metavar(self.metavar),
780801
help: self.help.clone(),
781802
});
782-
if self.strict {
783-
Meta::Strict(Box::new(meta))
784-
} else {
785-
meta
803+
match self.position {
804+
Position::Strict => Meta::Strict(Box::new(meta)),
805+
_ => meta,
786806
}
787807
}
788808
}
789809

790810
fn parse_pos_word(
791811
args: &mut State,
792-
strict: bool,
793-
metavar: &'static str,
812+
metavar: Metavar,
794813
help: &Option<Doc>,
814+
position: &Position,
795815
) -> Result<OsString, Error> {
796-
let metavar = Metavar(metavar);
797816
match args.take_positional_word(metavar) {
798817
Ok((ix, is_strict, word)) => {
799-
if strict && !is_strict {
800-
#[cfg(feature = "autocomplete")]
801-
args.push_pos_sep();
802-
803-
return Err(Error(Message::StrictPos(ix, metavar)));
818+
match position {
819+
&Position::Strict if !is_strict => {
820+
#[cfg(feature = "autocomplete")]
821+
args.push_pos_sep();
822+
return Err(Error(Message::StrictPos(ix, metavar)));
823+
}
824+
&Position::NonStrict if is_strict => {
825+
#[cfg(feature = "autocomplete")]
826+
args.push_pos_sep();
827+
return Err(Error(Message::NonStrictPos(ix, metavar)));
828+
}
829+
_ => {}
804830
}
831+
805832
#[cfg(feature = "autocomplete")]
806833
if args.touching_last_remove() && !args.check_no_pos_ahead() {
807834
args.push_metavar(metavar.0, help, false);
@@ -826,13 +853,14 @@ where
826853
<T as std::str::FromStr>::Err: std::fmt::Display,
827854
{
828855
fn eval(&self, args: &mut State) -> Result<T, Error> {
829-
let os = parse_pos_word(args, self.strict, self.metavar, &self.help)?;
856+
let os = parse_pos_word(args, Metavar(self.metavar), &self.help, &self.position)?;
830857
match parse_os_str::<T>(os) {
831858
Ok(ok) => Ok(ok),
832859
Err(err) => Err(Error(Message::ParseFailed(args.current, err))),
833860
}
834861
}
835862

863+
#[inline(always)]
836864
fn meta(&self) -> Meta {
837865
self.meta()
838866
}

tests/positionals.rs

+14
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,17 @@ fn strictly_positional() {
144144
let r = parser.run_inner(&["--"]).unwrap_err().unwrap_stderr();
145145
assert_eq!(r, "expected `A`, pass `--help` for usage information");
146146
}
147+
148+
#[test]
149+
fn non_strictly_positional() {
150+
let parser = positional::<String>("A").non_strict().to_options();
151+
152+
let r = parser.run_inner(&["a"]).unwrap();
153+
assert_eq!(r, "a");
154+
155+
let r = parser.run_inner(&["--", "a"]).unwrap_err().unwrap_stderr();
156+
assert_eq!(r, "expected `A` to be on the left side of `--`");
157+
158+
let r = parser.run_inner(&["--"]).unwrap_err().unwrap_stderr();
159+
assert_eq!(r, "expected `A`, pass `--help` for usage information");
160+
}

0 commit comments

Comments
 (0)