Skip to content

Commit

Permalink
Merge pull request #376 from pacak/ftu
Browse files Browse the repository at this point in the history
Support `fallback_to_usage` in derive macro
  • Loading branch information
pacak authored Jul 22, 2024
2 parents 7a3d13a + 215a78e commit 74ef468
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Change Log

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

## bpaf [0.9.12] - 2024-04-29
- better error messages
Expand Down
22 changes: 22 additions & 0 deletions bpaf_derive/src/td.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub(crate) struct OptionsCfg {
pub(crate) usage: Option<Box<Expr>>,
pub(crate) version: Option<Box<Expr>>,
pub(crate) max_width: Option<Box<Expr>>,
pub(crate) fallback_usage: bool,
}

#[derive(Debug, Default)]
Expand Down Expand Up @@ -217,6 +218,15 @@ impl Parse for TopInfo {
boxed = true;
} else if kw == "adjacent" {
adjacent = true;
} else if kw == "fallback_to_usage" {
if let Some(opts) = options.as_mut() {
opts.fallback_usage = true;
} else {
return Err(Error::new_spanned(
kw,
"This annotation only makes sense in combination with `options` or `command`",
));
}
} else if kw == "short" {
let short = parse_arg(input)?;
with_command(&kw, command.as_mut(), |cfg| cfg.short.push(short))?;
Expand Down Expand Up @@ -338,6 +348,15 @@ impl Parse for Ed {
} else {
attrs.push(EAttr::UnitLong(parse_opt_arg(input)?));
}
} else if kw == "fallback_to_usage" {
if matches!(mode, VariantMode::Command) {
attrs.push(EAttr::FallbackUsage);
} else {
return Err(Error::new_spanned(
kw,
"In this context this attribute requires \"command\" annotation",
));
}
} else if kw == "skip" {
skip = true;
} else if kw == "adjacent" {
Expand Down Expand Up @@ -375,6 +394,7 @@ pub(crate) enum EAttr {
NamedCommand(LitStr),
UnnamedCommand,

FallbackUsage,
CommandShort(LitChar),
CommandLong(LitStr),
Adjacent,
Expand Down Expand Up @@ -403,6 +423,8 @@ impl ToTokens for EAttr {
Self::Usage(u) => quote!(usage(#u)),
Self::Env(e) => quote!(env(#e)),
Self::Hide => quote!(hide()),
Self::FallbackUsage => quote!(fallback_to_usage()),

Self::UnnamedCommand | Self::UnitShort(_) | Self::UnitLong(_) => unreachable!(),
}
.to_tokens(tokens);
Expand Down
22 changes: 20 additions & 2 deletions bpaf_derive/src/top.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ impl ToTokens for Top {
footer,
header,
max_width,
fallback_usage,
} = options;

let version = version.as_ref().map(|v| quote!(.version(#v)));
Expand All @@ -206,7 +207,11 @@ impl ToTokens for Top {
let footer = footer.as_ref().map(|v| quote!(.footer(#v)));
let header = header.as_ref().map(|v| quote!(.header(#v)));
let max_width = max_width.as_ref().map(|v| quote!(.max_width(#v)));

let fallback_usage = if *fallback_usage {
Some(quote!(.fallback_to_usage()))
} else {
None
};
let CommandCfg {
name,
long,
Expand All @@ -225,6 +230,7 @@ impl ToTokens for Top {
#body
#(.#attrs)*
.to_options()
#fallback_usage
#version
#descr
#header
Expand All @@ -249,12 +255,18 @@ impl ToTokens for Top {
footer,
header,
max_width,
fallback_usage,
} = options;
let body = match cargo_helper {
Some(cargo) => quote!(::bpaf::cargo_helper(#cargo, #body)),
None => quote!(#body),
};

let fallback_usage = if *fallback_usage {
Some(quote!(.fallback_to_usage()))
} else {
None
};
let version = version.as_ref().map(|v| quote!(.version(#v)));
let usage = usage.as_ref().map(|v| quote!(.usage(#v)));
let descr = descr.as_ref().map(|v| quote!(.descr(#v)));
Expand All @@ -269,6 +281,7 @@ impl ToTokens for Top {
#body
#(.#attrs)*
.to_options()
#fallback_usage
#version
#descr
#header
Expand Down Expand Up @@ -444,7 +457,7 @@ impl ParsedEnumBranch {

let mut attrs = Vec::with_capacity(ea.len());
let mut has_options = None;

let mut fallback_usage = false;
for attr in ea {
match attr {
EAttr::NamedCommand(_) => {
Expand Down Expand Up @@ -487,11 +500,16 @@ impl ParsedEnumBranch {
attrs.insert(o + 1, attr);
}
}
EAttr::FallbackUsage => fallback_usage = true,
EAttr::ToOptions => unreachable!(),
}
}

if let Some(opts_at) = has_options {
if fallback_usage {
attrs.push(EAttr::FallbackUsage);
}

if let Some(h) = std::mem::take(&mut help) {
split_ehelp_into(h, opts_at, &mut attrs);
}
Expand Down
46 changes: 46 additions & 0 deletions bpaf_derive/src/top_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,52 @@ fn cargo_command_helper() {
assert_eq!(top.to_token_stream().to_string(), expected.to_string());
}

#[test]
fn fallback_usage_top() {
let top: Top = parse_quote! {
#[bpaf(options, fallback_to_usage)]
struct Opts {
verbose: bool
}
};

let expected = quote! {
fn opts() -> ::bpaf::OptionParser<Opts> {
#[allow(unused_imports)]
use ::bpaf::Parser;
{
let verbose = ::bpaf::long("verbose").switch();
::bpaf::construct!(Opts { verbose, })
}
.to_options()
.fallback_to_usage()
}
};
assert_eq!(top.to_token_stream().to_string(), expected.to_string());
}

#[test]
fn fallback_usage_subcommand() {
let input: Top = parse_quote! {
/// those are options
#[bpaf(command, fallback_to_usage)]
struct Opt;
};

let expected = quote! {
fn opt() -> impl ::bpaf::Parser<Opt> {
#[allow (unused_imports)]
use ::bpaf::Parser;
::bpaf::pure(Opt)
.to_options()
.fallback_to_usage()
.descr("those are options")
.command("opt")
}
};
assert_eq!(input.to_token_stream().to_string(), expected.to_string());
}

#[test]
fn top_struct_construct() {
let top: Top = parse_quote! {
Expand Down
28 changes: 28 additions & 0 deletions src/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,34 @@ impl<T> OptionParser<T> {
/// // create option parser in a usual way, derive or combinatoric API
/// let opts = options().fallback_to_usage().run();
/// ```
///
/// For derive macro you can specify `fallback_to_usage` in top level annotations
/// for options and for individual commands if fallback to useage is the desired behavior:
///
///
/// ```ignore
/// #[derive(Debug, Clone, Bpaf)]
/// enum Commands {
/// #[bpaf(command, fallback_to_usage)]
/// Action {
/// ...
/// }
/// }
/// ```
///
/// Or
///
/// ```ignore
/// #[derive(Debug, Clone, Bpaf)]
/// #[bpaf(options, fallback_to_usage)]
/// struct Options {
/// ...
/// }
///
/// fn main() {
/// let options = options().run(); // falls back to usage
/// }
/// ```
#[must_use]
pub fn fallback_to_usage(mut self) -> Self {
self.info.help_if_no_args = true;
Expand Down
28 changes: 28 additions & 0 deletions tests/help_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@ fn help_after_switch() {
assert_eq!(r, expected);
}

#[test]
fn fallback_to_usage() {
let a = short('a')
.argument::<usize>("A")
.to_options()
.fallback_to_usage();

let r = a.run_inner(&[]).unwrap_err().unwrap_stdout();
let expected =
"Usage: -a=A\n\nAvailable options:\n -a=A\n -h, --help Prints help information\n";
assert_eq!(r, expected);
}

#[test]
fn fallback_to_usage_nested() {
let a = short('a')
.argument::<usize>("A")
.to_options()
.fallback_to_usage()
.command("cmd")
.to_options();

let r = a.run_inner(&["cmd"]).unwrap_err().unwrap_stdout();
let expected =
"Usage: cmd -a=A\n\nAvailable options:\n -a=A\n -h, --help Prints help information\n";
assert_eq!(r, expected);
}

#[test]
fn fancy_meta() {
let a = long("trailing-comma").argument::<String>("all|es5|none");
Expand Down

0 comments on commit 74ef468

Please sign in to comment.