Repository: pacak/bpaf
Pull request #313 (2023-10-30 12:25:56)

Release 0.9.6
- make sure env-only arguments and flags are working
- support raw identifiers in derive macro (#282)
- better error messages for a few corner cases
- bugfix in completions generator for bash (#311)
- `choice` combinator to efficiently construct alternative parsers at runtime (#308)
- use `eprintln!` + `std::process::exit` instead of panic if shell completions must be regenerated for less jarring user experience.


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 0d39df6..bd67518 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf"
-version = "0.9.5"
+version = "0.9.6"
 edition = "2021"
 categories = ["command-line-interface"]
 description = "A simple Command Line Argument Parser with parser combinators"
@@ -19,7 +19,7 @@ include = [
 
 
 [dependencies]
-bpaf_derive = { path = "./bpaf_derive", version = "=0.5.5", optional = true }
+bpaf_derive = { path = "./bpaf_derive", version = "=0.5.6", optional = true }
 owo-colors = { version = "3.5.0", default-features = false, optional = true }
 supports-color = { version = "2.0.0", optional = true }
 
diff --git a/Changelog.md b/Changelog.md
index 2589187..bb39840 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,9 +1,12 @@
 # Change Log
 
-## bpaf [0.9.6], bpaf_derive [0.5.6] - Unreleased
+## bpaf [0.9.6], bpaf_derive [0.5.6] - 2023-10-30
 - make sure env-only arguments and flags are working
 - support raw identifiers in derive macro (#282)
 - better error messages for unexpected values that prevent positional parses
+- bugfix in completions generator for bash
+  thanks @akinomyoga
+- `choice` combinator to efficiently construct alternative parsers at runtime
 
 ## bpaf [0.9.5], bpaf_derive [0.5.5] - 2023-08-24
 - fancier squashing: parse `-abfoo` as `-a -b=foo` if b is a short argument
diff --git a/bpaf_derive/Cargo.toml b/bpaf_derive/Cargo.toml
index 58b8027..ee3c07e 100644
--- a/bpaf_derive/Cargo.toml
+++ b/bpaf_derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf_derive"
-version = "0.5.5"
+version = "0.5.6"
 edition = "2018"
 categories = ["command-line-interface"]
 description = "Derive macros for bpaf Command Line Argument Parser"
diff --git a/docs2/src/choice/cases.md b/docs2/src/choice/cases.md
new file mode 100644
index 0000000..eb4b14c
--- /dev/null
+++ b/docs2/src/choice/cases.md
@@ -0,0 +1,15 @@
+Here [`choice`] function is used to create an option for each possible desert item
+
+> --help
+
+User can pick any item
+
+> --apple
+
+Since parser consumes only one value you can't specify multiple flags of the same type
+
+> --orange --grape
+
+And [`Parser::optional`] makes it so when value is not specified - `None` is produced instead
+
+>
diff --git a/docs2/src/choice/combine.rs b/docs2/src/choice/combine.rs
new file mode 100644
index 0000000..bc763c5
--- /dev/null
+++ b/docs2/src/choice/combine.rs
@@ -0,0 +1,20 @@
+//
+use bpaf::*;
+
+#[derive(Debug, Clone)]
+pub struct Options {
+    desert: Option<&'static str>,
+}
+
+pub fn options() -> OptionParser<Options> {
+    let desert = ["apple", "banana", "orange", "grape", "strawberry"]
+        .iter()
+        .map(|name| {
+            long(name)
+                .help("Pick one of the options")
+                .req_flag(*name)
+                .boxed()
+        });
+    let desert = choice(desert).optional();
+    construct!(Options { desert }).to_options()
+}
diff --git a/src/complete_gen.rs b/src/complete_gen.rs
index ee2896b..cf1c846 100644
--- a/src/complete_gen.rs
+++ b/src/complete_gen.rs
@@ -403,7 +403,10 @@ impl State {
             7 => render_zsh(&items, &shell, full_lit),
             8 => render_bash(&items, &shell, full_lit),
             9 => render_fish(&items, &shell, full_lit, self.path[0].as_str()),
-            unk => panic!("Unsupported output revision {}, you need to genenerate your shell completion files for the app", unk)
+            unk => {
+                eprintln!("Unsupported output revision {}, you need to genenerate your shell completion files for the app", unk);
+                std::process::exit(1);
+            }
         }.unwrap())
     }
 }
diff --git a/src/docs2/choice.md b/src/docs2/choice.md
new file mode 100644
index 0000000..8999a18
--- /dev/null
+++ b/src/docs2/choice.md
@@ -0,0 +1,102 @@
+
+```no_run
+
+#[derive(Debug, Clone)]
+pub struct Options {
+    desert: Option<&'static str>,
+}
+
+pub fn options() -> OptionParser<Options> {
+    let desert = ["apple", "banana", "orange", "grape", "strawberry"]
+        .iter()
+        .map(|name| {
+            long(name)
+                .help("Pick one of the options")
+                .req_flag(*name)
+                .boxed()
+        });
+    let desert = choice(desert).optional();
+    construct!(Options { desert }).to_options()
+}
+
+fn main() {
+    println!("{:?}", options().run())
+}
+```
+
+<details><summary>Output</summary>
+
+Here [`choice`] function is used to create an option for each possible desert item
+
+
+<div class='bpaf-doc'>
+$ app --help<br>
+<p><b>Usage</b>: <tt><b>app</b></tt> [<tt><b>--apple</b></tt> | <tt><b>--banana</b></tt> | <tt><b>--orange</b></tt> | <tt><b>--grape</b></tt> | <tt><b>--strawberry</b></tt>]</p><p><div>
+<b>Available options:</b></div><dl><dt><tt><b>    --apple</b></tt></dt>
+<dd>Pick one of the options</dd>
+<dt><tt><b>    --banana</b></tt></dt>
+<dd>Pick one of the options</dd>
+<dt><tt><b>    --orange</b></tt></dt>
+<dd>Pick one of the options</dd>
+<dt><tt><b>    --grape</b></tt></dt>
+<dd>Pick one of the options</dd>
+<dt><tt><b>    --strawberry</b></tt></dt>
+<dd>Pick one of the options</dd>
+<dt><tt><b>-h</b></tt>, <tt><b>--help</b></tt></dt>
+<dd>Prints help information</dd>
+</dl>
+</p>
+<style>
+div.bpaf-doc {
+    padding: 14px;
+    background-color:var(--code-block-background-color);
+    font-family: "Source Code Pro", monospace;
+    margin-bottom: 0.75em;
+}
+div.bpaf-doc dt { margin-left: 1em; }
+div.bpaf-doc dd { margin-left: 3em; }
+div.bpaf-doc dl { margin-top: 0; padding-left: 1em; }
+div.bpaf-doc  { padding-left: 1em; }
+</style>
+</div>
+
+
+User can pick any item
+
+
+<div class='bpaf-doc'>
+$ app --apple<br>
+Options { desert: Some("apple") }
+</div>
+
+
+Only one value is consumed at once
+
+
+<div class='bpaf-doc'>
+$ app --orange --grape<br>
+<b>Error:</b> <tt><b>--grape</b></tt> cannot be used at the same time as <tt><b>--orange</b></tt>
+<style>
+div.bpaf-doc {
+    padding: 14px;
+    background-color:var(--code-block-background-color);
+    font-family: "Source Code Pro", monospace;
+    margin-bottom: 0.75em;
+}
+div.bpaf-doc dt { margin-left: 1em; }
+div.bpaf-doc dd { margin-left: 3em; }
+div.bpaf-doc dl { margin-top: 0; padding-left: 1em; }
+div.bpaf-doc  { padding-left: 1em; }
+</style>
+</div>
+
+
+And [`Parser::optional`] makes it so when value is not specified - `None` is produced instead
+
+
+<div class='bpaf-doc'>
+$ app <br>
+Options { desert: None }
+</div>
+
+</details>
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index a60d727..633a833 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1532,3 +1532,22 @@ where
     let skip = literal(cmd).optional().hide();
     construct!(skip, parser).map(|x| x.1)
 }
+
+/// Choose between several parsers specified at runtime
+///
+/// You can use this function to create multiple parsers that produce the same type of value at a runtime
+/// and let bpaf to pick one that best fits best. This function is designed to work in Combinatoric
+/// API, but you can use it in Derive API with `extern`.
+///
+#[cfg_attr(not(doctest), doc = include_str!("docs2/choice.md"))]
+pub fn choice<T: 'static>(parsers: impl IntoIterator<Item = Box<dyn Parser<T>>>) -> impl Parser<T> {
+    let mut parsers = parsers.into_iter();
+    let mut this = match parsers.next() {
+        None => return fail("Invalid choice usage").boxed(),
+        Some(p) => p,
+    };
+    for that in parsers {
+        this = Box::new(ParseOrElse { this, that })
+    }
+    this
+}
diff --git a/src/meta_youmean.rs b/src/meta_youmean.rs
index c6ccbdd..c5b2224 100644
--- a/src/meta_youmean.rs
+++ b/src/meta_youmean.rs
@@ -29,6 +29,10 @@ pub(crate) fn suggest(args: &State, meta: &Meta) -> Option<(usize, Suggestion)>
     if arg.os_str().is_empty() {
         return None;
     }
+    // don't try to suggest fixes for typos in strictly positional items
+    if matches!(arg, crate::args::Arg::PosWord(_)) {
+        return None;
+    }
     // it also should be a printable name
     let actual = arg.to_string();
 


Test patch:
diff --git a/tests/errors.rs b/tests/errors.rs
index f55eff5..4ee4fc9 100644
--- a/tests/errors.rs
+++ b/tests/errors.rs
@@ -231,3 +231,14 @@ fn pos_with_invalid_arg() {
     let r = parser.run_inner(&["t", "-c"]).unwrap_err().unwrap_stderr();
     assert_eq!(r, "couldn't parse `t`: invalid digit found in string");
 }
+
+#[test]
+fn strictly_positional_help() {
+    let parser = long("hhhh").switch().to_options();
+
+    let r = parser
+        .run_inner(&["--", "--help"])
+        .unwrap_err()
+        .unwrap_stderr();
+    assert_eq!(r, "`--help` is not expected in this context");
+}