Skip to content

Commit 74ef468

Browse files
authored
Merge pull request #376 from pacak/ftu
Support `fallback_to_usage` in derive macro
2 parents 7a3d13a + 215a78e commit 74ef468

File tree

6 files changed

+146
-2
lines changed

6 files changed

+146
-2
lines changed

Changelog.md

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Change Log
22

3+
## bpaf [0.9.13] - Unreleased
4+
- You can now use `fallback_to_usage` in derive macro for options and subcommands
35

46
## bpaf [0.9.12] - 2024-04-29
57
- better error messages

bpaf_derive/src/td.rs

+22
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub(crate) struct OptionsCfg {
3030
pub(crate) usage: Option<Box<Expr>>,
3131
pub(crate) version: Option<Box<Expr>>,
3232
pub(crate) max_width: Option<Box<Expr>>,
33+
pub(crate) fallback_usage: bool,
3334
}
3435

3536
#[derive(Debug, Default)]
@@ -217,6 +218,15 @@ impl Parse for TopInfo {
217218
boxed = true;
218219
} else if kw == "adjacent" {
219220
adjacent = true;
221+
} else if kw == "fallback_to_usage" {
222+
if let Some(opts) = options.as_mut() {
223+
opts.fallback_usage = true;
224+
} else {
225+
return Err(Error::new_spanned(
226+
kw,
227+
"This annotation only makes sense in combination with `options` or `command`",
228+
));
229+
}
220230
} else if kw == "short" {
221231
let short = parse_arg(input)?;
222232
with_command(&kw, command.as_mut(), |cfg| cfg.short.push(short))?;
@@ -338,6 +348,15 @@ impl Parse for Ed {
338348
} else {
339349
attrs.push(EAttr::UnitLong(parse_opt_arg(input)?));
340350
}
351+
} else if kw == "fallback_to_usage" {
352+
if matches!(mode, VariantMode::Command) {
353+
attrs.push(EAttr::FallbackUsage);
354+
} else {
355+
return Err(Error::new_spanned(
356+
kw,
357+
"In this context this attribute requires \"command\" annotation",
358+
));
359+
}
341360
} else if kw == "skip" {
342361
skip = true;
343362
} else if kw == "adjacent" {
@@ -375,6 +394,7 @@ pub(crate) enum EAttr {
375394
NamedCommand(LitStr),
376395
UnnamedCommand,
377396

397+
FallbackUsage,
378398
CommandShort(LitChar),
379399
CommandLong(LitStr),
380400
Adjacent,
@@ -403,6 +423,8 @@ impl ToTokens for EAttr {
403423
Self::Usage(u) => quote!(usage(#u)),
404424
Self::Env(e) => quote!(env(#e)),
405425
Self::Hide => quote!(hide()),
426+
Self::FallbackUsage => quote!(fallback_to_usage()),
427+
406428
Self::UnnamedCommand | Self::UnitShort(_) | Self::UnitLong(_) => unreachable!(),
407429
}
408430
.to_tokens(tokens);

bpaf_derive/src/top.rs

+20-2
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ impl ToTokens for Top {
198198
footer,
199199
header,
200200
max_width,
201+
fallback_usage,
201202
} = options;
202203

203204
let version = version.as_ref().map(|v| quote!(.version(#v)));
@@ -206,7 +207,11 @@ impl ToTokens for Top {
206207
let footer = footer.as_ref().map(|v| quote!(.footer(#v)));
207208
let header = header.as_ref().map(|v| quote!(.header(#v)));
208209
let max_width = max_width.as_ref().map(|v| quote!(.max_width(#v)));
209-
210+
let fallback_usage = if *fallback_usage {
211+
Some(quote!(.fallback_to_usage()))
212+
} else {
213+
None
214+
};
210215
let CommandCfg {
211216
name,
212217
long,
@@ -225,6 +230,7 @@ impl ToTokens for Top {
225230
#body
226231
#(.#attrs)*
227232
.to_options()
233+
#fallback_usage
228234
#version
229235
#descr
230236
#header
@@ -249,12 +255,18 @@ impl ToTokens for Top {
249255
footer,
250256
header,
251257
max_width,
258+
fallback_usage,
252259
} = options;
253260
let body = match cargo_helper {
254261
Some(cargo) => quote!(::bpaf::cargo_helper(#cargo, #body)),
255262
None => quote!(#body),
256263
};
257264

265+
let fallback_usage = if *fallback_usage {
266+
Some(quote!(.fallback_to_usage()))
267+
} else {
268+
None
269+
};
258270
let version = version.as_ref().map(|v| quote!(.version(#v)));
259271
let usage = usage.as_ref().map(|v| quote!(.usage(#v)));
260272
let descr = descr.as_ref().map(|v| quote!(.descr(#v)));
@@ -269,6 +281,7 @@ impl ToTokens for Top {
269281
#body
270282
#(.#attrs)*
271283
.to_options()
284+
#fallback_usage
272285
#version
273286
#descr
274287
#header
@@ -444,7 +457,7 @@ impl ParsedEnumBranch {
444457

445458
let mut attrs = Vec::with_capacity(ea.len());
446459
let mut has_options = None;
447-
460+
let mut fallback_usage = false;
448461
for attr in ea {
449462
match attr {
450463
EAttr::NamedCommand(_) => {
@@ -487,11 +500,16 @@ impl ParsedEnumBranch {
487500
attrs.insert(o + 1, attr);
488501
}
489502
}
503+
EAttr::FallbackUsage => fallback_usage = true,
490504
EAttr::ToOptions => unreachable!(),
491505
}
492506
}
493507

494508
if let Some(opts_at) = has_options {
509+
if fallback_usage {
510+
attrs.push(EAttr::FallbackUsage);
511+
}
512+
495513
if let Some(h) = std::mem::take(&mut help) {
496514
split_ehelp_into(h, opts_at, &mut attrs);
497515
}

bpaf_derive/src/top_tests.rs

+46
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,52 @@ fn cargo_command_helper() {
2626
assert_eq!(top.to_token_stream().to_string(), expected.to_string());
2727
}
2828

29+
#[test]
30+
fn fallback_usage_top() {
31+
let top: Top = parse_quote! {
32+
#[bpaf(options, fallback_to_usage)]
33+
struct Opts {
34+
verbose: bool
35+
}
36+
};
37+
38+
let expected = quote! {
39+
fn opts() -> ::bpaf::OptionParser<Opts> {
40+
#[allow(unused_imports)]
41+
use ::bpaf::Parser;
42+
{
43+
let verbose = ::bpaf::long("verbose").switch();
44+
::bpaf::construct!(Opts { verbose, })
45+
}
46+
.to_options()
47+
.fallback_to_usage()
48+
}
49+
};
50+
assert_eq!(top.to_token_stream().to_string(), expected.to_string());
51+
}
52+
53+
#[test]
54+
fn fallback_usage_subcommand() {
55+
let input: Top = parse_quote! {
56+
/// those are options
57+
#[bpaf(command, fallback_to_usage)]
58+
struct Opt;
59+
};
60+
61+
let expected = quote! {
62+
fn opt() -> impl ::bpaf::Parser<Opt> {
63+
#[allow (unused_imports)]
64+
use ::bpaf::Parser;
65+
::bpaf::pure(Opt)
66+
.to_options()
67+
.fallback_to_usage()
68+
.descr("those are options")
69+
.command("opt")
70+
}
71+
};
72+
assert_eq!(input.to_token_stream().to_string(), expected.to_string());
73+
}
74+
2975
#[test]
3076
fn top_struct_construct() {
3177
let top: Top = parse_quote! {

src/info.rs

+28
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,34 @@ impl<T> OptionParser<T> {
647647
/// // create option parser in a usual way, derive or combinatoric API
648648
/// let opts = options().fallback_to_usage().run();
649649
/// ```
650+
///
651+
/// For derive macro you can specify `fallback_to_usage` in top level annotations
652+
/// for options and for individual commands if fallback to useage is the desired behavior:
653+
///
654+
///
655+
/// ```ignore
656+
/// #[derive(Debug, Clone, Bpaf)]
657+
/// enum Commands {
658+
/// #[bpaf(command, fallback_to_usage)]
659+
/// Action {
660+
/// ...
661+
/// }
662+
/// }
663+
/// ```
664+
///
665+
/// Or
666+
///
667+
/// ```ignore
668+
/// #[derive(Debug, Clone, Bpaf)]
669+
/// #[bpaf(options, fallback_to_usage)]
670+
/// struct Options {
671+
/// ...
672+
/// }
673+
///
674+
/// fn main() {
675+
/// let options = options().run(); // falls back to usage
676+
/// }
677+
/// ```
650678
#[must_use]
651679
pub fn fallback_to_usage(mut self) -> Self {
652680
self.info.help_if_no_args = true;

tests/help_format.rs

+28
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,34 @@ fn help_after_switch() {
88
assert_eq!(r, expected);
99
}
1010

11+
#[test]
12+
fn fallback_to_usage() {
13+
let a = short('a')
14+
.argument::<usize>("A")
15+
.to_options()
16+
.fallback_to_usage();
17+
18+
let r = a.run_inner(&[]).unwrap_err().unwrap_stdout();
19+
let expected =
20+
"Usage: -a=A\n\nAvailable options:\n -a=A\n -h, --help Prints help information\n";
21+
assert_eq!(r, expected);
22+
}
23+
24+
#[test]
25+
fn fallback_to_usage_nested() {
26+
let a = short('a')
27+
.argument::<usize>("A")
28+
.to_options()
29+
.fallback_to_usage()
30+
.command("cmd")
31+
.to_options();
32+
33+
let r = a.run_inner(&["cmd"]).unwrap_err().unwrap_stdout();
34+
let expected =
35+
"Usage: cmd -a=A\n\nAvailable options:\n -a=A\n -h, --help Prints help information\n";
36+
assert_eq!(r, expected);
37+
}
38+
1139
#[test]
1240
fn fancy_meta() {
1341
let a = long("trailing-comma").argument::<String>("all|es5|none");

0 commit comments

Comments
 (0)