Repository: pacak/bpaf
Pull request #166 (2023-02-14 19:13:44)

Release 0.7.9 
- a way to override `usage` in derive macro
- `ParseFailure::exit_code`


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index c6610cc..e7b65e2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf"
-version = "0.7.8"
+version = "0.7.9"
 edition = "2021"
 categories = ["command-line-interface"]
 description = "A simple Command Line Argument Parser with parser combinators"
@@ -14,7 +14,7 @@ exclude = [".github/workflows", "tarp.sh"]
 
 
 [dependencies]
-bpaf_derive = { path = "./bpaf_derive", version = "=0.3.3", optional = true }
+bpaf_derive = { path = "./bpaf_derive", version = "=0.3.4", optional = true }
 owo-colors = { version = "3.5.0", features = ["supports-colors"], optional = true }
 roff = { version = "0.2.1", optional = true }
 
diff --git a/Changelog.md b/Changelog.md
index 23e8091..2a7d74f 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,6 +1,10 @@
 # Change Log
 
-## bpaf [0.7.8] - Unreleased
+## bpaf [0.7.9], bpaf_derive [0.3.4] - 2023-02-14
+- `ParseFailure::exit_code`
+- A way to specify custom usage in derive macro
+
+## bpaf [0.7.8] - 2023-01-01
 - manpage generation bugfixes,
   thanks to @ysndr
 - internal cleanups
diff --git a/bpaf_derive/Cargo.toml b/bpaf_derive/Cargo.toml
index 93dd012..bbed7f7 100644
--- a/bpaf_derive/Cargo.toml
+++ b/bpaf_derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf_derive"
-version = "0.3.3"
+version = "0.3.4"
 edition = "2018"
 categories = ["command-line-interface"]
 description = "Derive macros for bpaf Command Line Argument Parser"
diff --git a/bpaf_derive/src/top.rs b/bpaf_derive/src/top.rs
index 31cd1a4..da01ae5 100644
--- a/bpaf_derive/src/top.rs
+++ b/bpaf_derive/src/top.rs
@@ -58,6 +58,7 @@ struct Decor {
     header: Option<String>,
     footer: Option<String>,
     version: Option<Box<Expr>>,
+    usage: Option<LitStr>,
 }
 
 impl ToTokens for Decor {
@@ -80,6 +81,9 @@ impl ToTokens for Decor {
         if let Some(ver) = &self.version {
             quote!(.version(#ver)).to_tokens(tokens);
         }
+        if let Some(usage) = &self.usage {
+            quote!(.usage(#usage)).to_tokens(tokens);
+        }
     }
 }
 
@@ -121,6 +125,7 @@ pub struct CommandAttr {
     name: LitStr,
     shorts: Vec<LitChar>,
     longs: Vec<LitStr>,
+    usage: Option<LitStr>,
 }
 
 #[derive(Debug, Clone)]
@@ -169,6 +174,7 @@ impl Inner {
                             name,
                             shorts: Vec::new(),
                             longs: Vec::new(),
+                            usage: None,
                         });
                     } else if keyword == "short" {
                         let lit = parse_opt_arg::<LitChar>(input)?.unwrap_or_else(|| {
@@ -191,6 +197,15 @@ impl Inner {
                         res.is_default = true;
                     } else if keyword == "skip" {
                         res.skip = true;
+                    } else if keyword == "usage" {
+                        if let Some(cmd) = &mut res.command {
+                            let content;
+                            let _ = parenthesized!(content in input);
+                            cmd.usage = Some(content.parse::<LitStr>()?);
+                        } else {
+                            return Err(input_copy
+                                .error("In this context `usage` requires `command` annotation"));
+                        }
                     } else {
                         return Err(input_copy.error("Not a valid inner attribute"));
                     }
@@ -238,6 +253,7 @@ impl Outer {
             adjacent: false,
         };
 
+        let mut usage = None;
         let mut help = Vec::new();
         for attr in attrs {
             if attr.path.is_ident("doc") {
@@ -282,7 +298,17 @@ impl Outer {
                             name,
                             shorts: Vec::new(),
                             longs: Vec::new(),
+                            usage: None,
                         }));
+                    } else if keyword == "usage" {
+                        let content;
+                        let _ = parenthesized!(content in input);
+                        let lit = Some(content.parse::<LitStr>()?);
+                        if let Some(OuterKind::Command(cmd)) = &mut res.kind {
+                            cmd.usage = lit;
+                        } else {
+                            usage = lit;
+                        }
                     } else if keyword == "short" {
                         // those are aliaes, no fancy name figuring out logic
                         let lit = parse_lit_char(input)?;
@@ -310,7 +336,7 @@ impl Outer {
             cmd.longs.append(&mut res.longs);
         }
 
-        res.decor = Decor::new(&help, res.version.take());
+        res.decor = Decor::new(&help, res.version.take(), usage);
 
         Ok(res)
     }
@@ -426,7 +452,7 @@ impl Top {
 
             if !inner.skip {
                 if let Some(cmd_arg) = inner.command {
-                    let decor = Decor::new(&inner.help, None);
+                    let decor = Decor::new(&inner.help, None, None);
                     let oparser = OParser {
                         inner: Box::new(branch),
                         decor,
@@ -518,14 +544,18 @@ impl ToTokens for BParser {
                     names = quote!(#names .long(#long));
                 }
 
+                let usage = match &cmd_attr.usage {
+                    Some(usage) => quote!(.usage(#usage)),
+                    None => quote!(),
+                };
                 if let Some(msg) = &oparser.decor.descr {
                     quote!( {
-                        let inner_cmd = #oparser;
+                        let inner_cmd = #oparser #usage;
                         ::bpaf::command(#cmd_name, inner_cmd).help(#msg)#names
                     })
                 } else {
                     quote!({
-                        let inner_cmd = #oparser;
+                        let inner_cmd = #oparser #usage;
                         ::bpaf::command(#cmd_name, inner_cmd)#names
                     })
                 }
@@ -646,13 +676,14 @@ impl Fields {
 }
 
 impl Decor {
-    fn new(help: &[String], version: Option<Box<Expr>>) -> Self {
+    fn new(help: &[String], version: Option<Box<Expr>>, usage: Option<LitStr>) -> Self {
         let mut iter = LineIter::from(help);
         Decor {
             descr: iter.next(),
             header: iter.next(),
             footer: iter.next(),
             version,
+            usage,
         }
     }
 }
diff --git a/examples/very_custom_usage.rs b/examples/very_custom_usage.rs
new file mode 100644
index 0000000..a45e60d
--- /dev/null
+++ b/examples/very_custom_usage.rs
@@ -0,0 +1,36 @@
+//! A way to customize usage for a nested command
+//!
+//! Usually you would go with generated usage or by overriding it using `usage` attribute
+//! to the top level or command level bpaf annotation. By taking advantage of command being just a
+//! set of options with it's own help message and a custom prefix you can override the usage with
+//! an arbitrary string, including one generated at runtime by doing something like this:
+
+use bpaf::*;
+
+// this defines top level set of options and refers to an external parser `cmd_usage`
+// At this point cmd_usage can be any parser that produces Cmd
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(options)]
+enum Opts {
+    Cmd(#[bpaf(external(cmd_usage))] Cmd),
+}
+
+// bpaf defines command as something with its own help message that can be accessed with a
+// positional command name - inside of the command there is an OptionParser, this struct
+// defines the parser we are going to use later
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(options)]
+struct Cmd {
+    opt: bool,
+}
+
+// At this point we have OptionParser<Cmd> and we want to turn that into a regular parser
+// with custom usage string - for that we are using two functions from combinatoric api:
+// `usage` and `command`
+fn cmd_usage() -> impl Parser<Cmd> {
+    cmd().usage("A very custom usage goes here").command("cmd")
+}
+
+fn main() {
+    println!("{:?}", opts().run());
+}
diff --git a/src/batteries.rs b/src/batteries.rs
index c5c6564..5ba9264 100644
--- a/src/batteries.rs
+++ b/src/batteries.rs
@@ -165,3 +165,29 @@ where
         .hide();
     construct!(skip, parser).map(|x| x.1)
 }
+
+/// Get usage for a parser
+///
+/// In some cases you might want to print usage if user gave no command line options, in this case
+/// you should add an enum variant to a top level enum, make it hidden with `#[bpaf(hide)]`, make
+/// it default for the top level parser with something like `#[bpaf(fallback(Arg::Help))]`.
+///
+/// When handling cases you can do something like this for `Help` variant:
+///
+/// ```ignore
+///     ...
+///     Arg::Help => {
+///         println!("{}", get_usage(parser()));
+///         std::process::exit(0);
+///     }
+///     ...
+/// ```
+pub fn get_usage<T>(parser: crate::OptionParser<T>) -> String
+where
+    T: std::fmt::Debug,
+{
+    parser
+        .run_inner(crate::Args::from(&["--help"]))
+        .unwrap_err()
+        .unwrap_stdout()
+}
diff --git a/src/info.rs b/src/info.rs
index a5f6518..275fbd1 100644
--- a/src/info.rs
+++ b/src/info.rs
@@ -131,14 +131,7 @@ impl<T> OptionParser<T> {
     {
         match self.try_run() {
             Ok(t) => t,
-            Err(ParseFailure::Stdout(msg)) => {
-                print!("{}", msg); // completions are sad otherwise
-                std::process::exit(0);
-            }
-            Err(ParseFailure::Stderr(msg)) => {
-                eprintln!("{}", msg);
-                std::process::exit(1);
-            }
+            Err(err) => std::process::exit(err.exit_code()),
         }
     }
 
@@ -599,7 +592,15 @@ impl<T> OptionParser<T> {
     ///
     /// # Derive usage
     ///
-    /// Not available directly, but you can call `usage` on generated [`OptionParser`].
+    /// ```rust
+    /// # use bpaf::*;
+    /// #[derive(Debug, Clone, Bpaf)]
+    /// #[bpaf(options, usage("Usage: my_program: {usage}"))]
+    /// struct Options {
+    ///     #[bpaf(short)]
+    ///     switch: bool
+    /// }
+    /// ```
     #[must_use]
     pub fn usage(mut self, usage: &'static str) -> Self {
         self.info.usage = Some(usage);
diff --git a/src/lib.rs b/src/lib.rs
index 62c8c0a..4d8992c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1643,6 +1643,22 @@ impl ParseFailure {
             }
         }
     }
+
+    /// Run an action appropriate to the failure and produce the exit code
+    ///
+    /// Prints a message to `stdout` or `stderr` and returns the exit code
+    pub fn exit_code(self) -> i32 {
+        match self {
+            ParseFailure::Stdout(msg) => {
+                print!("{}", msg); // completions are sad otherwise
+                0
+            }
+            ParseFailure::Stderr(msg) => {
+                eprintln!("{}", msg);
+                1
+            }
+        }
+    }
 }
 
 /// Strip a command name if present at the front when used as a `cargo` command


Test patch:
diff --git a/bpaf_derive/src/top_tests.rs b/bpaf_derive/src/top_tests.rs
index 4d4b4ed..bb84b4a 100644
--- a/bpaf_derive/src/top_tests.rs
+++ b/bpaf_derive/src/top_tests.rs
@@ -92,6 +92,28 @@ fn top_struct_options1() {
     assert_eq!(top.to_token_stream().to_string(), expected.to_string());
 }
 
+#[test]
+fn options_with_custom_usage() {
+    let top: Top = parse_quote! {
+        #[bpaf(options, usage("App: {usage}"))]
+        struct Opt {}
+    };
+
+    let expected = quote! {
+        fn opt() -> ::bpaf::OptionParser<Opt> {
+            #[allow (unused_imports)]
+            use ::bpaf::Parser;
+                {
+                    ::bpaf::construct!(Opt {})
+                }
+                .to_options()
+                .usage("App: {usage}")
+        }
+    };
+
+    assert_eq!(top.to_token_stream().to_string(), expected.to_string());
+}
+
 #[test]
 fn struct_options2() {
     let input: Top = parse_quote! {
@@ -280,7 +302,7 @@ fn enum_to_flag_and_switches() {
             BarFoo,
             Baz(#[bpaf(long("bazz"))] String),
             Strange { strange: String },
-            #[bpaf(command("alpha"))]
+            #[bpaf(command("alpha"), usage("custom"))]
             Alpha,
             #[bpaf(command)]
             Omega,
@@ -304,7 +326,7 @@ fn enum_to_flag_and_switches() {
                     ::bpaf::construct!(Opt::Strange { strange })
                 };
                 let alt5 = {
-                    let inner_cmd = ::bpaf::pure(Opt::Alpha).to_options();
+                    let inner_cmd = ::bpaf::pure(Opt::Alpha).to_options().usage("custom");
                     ::bpaf::command("alpha", inner_cmd)
                 };
                 let alt6 = {