Repository: pacak/bpaf
Pull request #119 (2022-11-04 16:15:54)

Complete file
Fixes https://github.com/pacak/bpaf/issues/109


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index acbb2b9..e43a265 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf"
-version = "0.7.2"
+version = "0.7.3"
 edition = "2021"
 categories = ["command-line-interface"]
 description = "A simple Command Line Argument Parser with parser combinators"
diff --git a/Changelog.md b/Changelog.md
index d2dd48d..77763d7 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,9 +1,11 @@
 # Change Log
 
-## bpaf [0.7.3] - unreleased
+## bpaf [0.7.3] - 2022-11-14
 - `try_run`
    thanks to @ysndr
 - `-Obits=2048` is now parsed as short flag `O` with a value of `bits=2048` instad of crashing
+- `complete_shell` - a way to call to static shell completion functions, bash and zsh only for
+  now
 
 ## bpaf [0.7.2] - 2022-10-27
 - drop tainting logic, should be redundant
diff --git a/bpaf_cauwugo/Cargo.toml b/bpaf_cauwugo/Cargo.toml
index f6deea0..2d361fb 100644
--- a/bpaf_cauwugo/Cargo.toml
+++ b/bpaf_cauwugo/Cargo.toml
@@ -12,7 +12,7 @@ description = "A cargo frontend with dynamic completion"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-bpaf = { version = "0.7.2", path = "../", features = ["derive", "autocomplete"] }
+bpaf = { version = "0.7.3", path = "../", features = ["derive", "autocomplete"] }
 cargo_metadata = "0.15.0"
 once_cell = "1.13.1"
 
diff --git a/bpaf_derive/src/field.rs b/bpaf_derive/src/field.rs
index f8b66b1..45314cd 100644
--- a/bpaf_derive/src/field.rs
+++ b/bpaf_derive/src/field.rs
@@ -12,7 +12,7 @@ use crate::utils::to_kebab_case;
 pub struct ConstrName {
     pub namespace: Option<Ident>,
     pub constr: Ident,
-    pub fallback: Option<Expr>,
+    pub fallback: Option<Box<Expr>>,
 }
 
 mod named_field;
@@ -48,6 +48,7 @@ enum PostprAttr {
     Fallback(Span, Box<Expr>),
     FallbackWith(Span, Box<Expr>),
     Complete(Span, Path),
+    CompleteShell(Span, Box<Expr>),
     Hide(Span),
     HideUsage(Span),
     GroupHelp(Span, Box<Expr>),
@@ -65,6 +66,7 @@ impl PostprAttr {
             | PostprAttr::Fallback(..)
             | PostprAttr::FallbackWith(..)
             | PostprAttr::Complete(..)
+            | PostprAttr::CompleteShell(..)
             | PostprAttr::Hide(..)
             | PostprAttr::HideUsage(..)
             | PostprAttr::Catch(..)
@@ -82,6 +84,7 @@ impl PostprAttr {
             | PostprAttr::Fallback(span, _)
             | PostprAttr::FallbackWith(span, _)
             | PostprAttr::Complete(span, _)
+            | PostprAttr::CompleteShell(span, _)
             | PostprAttr::Hide(span)
             | PostprAttr::HideUsage(span)
             | PostprAttr::Catch(span)
@@ -208,6 +211,7 @@ impl ToTokens for PostprAttr {
             PostprAttr::HideUsage(_span) => quote!(hide_usage()),
             PostprAttr::Catch(_span) => quote!(catch()),
             PostprAttr::Complete(_span, f) => quote!(complete(#f)),
+            PostprAttr::CompleteShell(_span, f) => quote!(complete_shell(#f)),
             PostprAttr::GroupHelp(_span, m) => quote!(group_help(#m)),
         }
         .to_tokens(tokens);
diff --git a/bpaf_derive/src/field/named_field.rs b/bpaf_derive/src/field/named_field.rs
index 99e2fa8..b2f2056 100644
--- a/bpaf_derive/src/field/named_field.rs
+++ b/bpaf_derive/src/field/named_field.rs
@@ -90,8 +90,8 @@ pub fn parse_path(input: ParseStream) -> Result<Path> {
 }
 
 #[inline(never)]
-pub fn parse_expr(input: ParseStream) -> Result<Expr> {
-    parse_arg(input)
+pub fn parse_expr(input: ParseStream) -> Result<Box<Expr>> {
+    Ok(Box::new(parse_arg(input)?))
 }
 
 impl Field {
@@ -161,7 +161,7 @@ impl Field {
                     } else if keyword == "env" {
                         check_stage(&mut stage, 1, &keyword)?;
 
-                        res.env = Some(Box::new(parse_expr(input)?));
+                        res.env = Some(parse_expr(input)?);
                     //
                     // consumer
                     } else if keyword == "argument" {
@@ -246,11 +246,11 @@ impl Field {
                     } else if keyword == "fallback" {
                         check_stage(&mut stage, 4, &keyword)?;
                         res.postpr
-                            .push(PostprAttr::Fallback(span, Box::new(parse_expr(input)?)));
+                            .push(PostprAttr::Fallback(span, parse_expr(input)?));
                     } else if keyword == "fallback_with" {
                         check_stage(&mut stage, 4, &keyword)?;
                         res.postpr
-                            .push(PostprAttr::FallbackWith(span, Box::new(parse_expr(input)?)));
+                            .push(PostprAttr::FallbackWith(span, parse_expr(input)?));
                     } else if keyword == "parse" {
                         check_stage(&mut stage, 4, &keyword)?;
                         res.postpr.push(PostprAttr::Parse(span, parse_path(input)?));
@@ -261,6 +261,10 @@ impl Field {
                         check_stage(&mut stage, 4, &keyword)?;
                         let f = parse_path(input)?;
                         res.postpr.push(PostprAttr::Complete(span, f));
+                    } else if keyword == "complete_shell" {
+                        check_stage(&mut stage, 4, &keyword)?;
+                        let f = parse_expr(input)?;
+                        res.postpr.push(PostprAttr::CompleteShell(span, f));
                     } else if keyword == "many" {
                         check_stage(&mut stage, 4, &keyword)?;
                         res.postpr.push(PostprAttr::Many(span, None));
@@ -283,7 +287,7 @@ impl Field {
                     } else if keyword == "group_help" {
                         check_stage(&mut stage, 4, &keyword)?;
                         let expr = parse_expr(input)?;
-                        res.postpr.push(PostprAttr::GroupHelp(span, Box::new(expr)));
+                        res.postpr.push(PostprAttr::GroupHelp(span, expr));
                     } else {
                         return Err(input_copy.error("Unexpected attribute"));
                     }
diff --git a/bpaf_derive/src/top.rs b/bpaf_derive/src/top.rs
index ad6dcfb..31cd1a4 100644
--- a/bpaf_derive/src/top.rs
+++ b/bpaf_derive/src/top.rs
@@ -43,7 +43,7 @@ enum BParser {
     CompStyle(Box<Expr>, Box<BParser>),
     Constructor(ConstrName, Fields, bool),
     Singleton(Box<ReqFlag>),
-    Fold(Vec<BParser>, Option<Expr>),
+    Fold(Vec<BParser>, Option<Box<Expr>>),
 }
 
 #[derive(Debug)]
@@ -184,7 +184,7 @@ impl Inner {
                         res.longs.push(lit);
                     } else if keyword == "env" {
                         let lit = parse_expr(input)?;
-                        res.envs.push(lit);
+                        res.envs.push(*lit);
                     } else if keyword == "hide" {
                         res.is_hidden = true;
                     } else if keyword == "default" {
@@ -214,12 +214,12 @@ struct Outer {
     kind: Option<OuterKind>,
     version: Option<Box<Expr>>,
     vis: Visibility,
-    comp_style: Option<Expr>,
+    comp_style: Option<Box<Expr>>,
     generate: Option<Ident>,
     decor: Decor,
     longs: Vec<LitStr>,
     shorts: Vec<LitChar>,
-    fallback: Option<Expr>,
+    fallback: Option<Box<Expr>>,
     adjacent: bool,
 }
 
@@ -335,7 +335,7 @@ impl Parse for Top {
 
 fn decorate_with_kind(outer: Outer, inner: BParser) -> ParserKind {
     let inner = if let Some(comp_style) = outer.comp_style {
-        BParser::CompStyle(Box::new(comp_style), Box::new(inner))
+        BParser::CompStyle(comp_style, Box::new(inner))
     } else {
         inner
     };
diff --git a/examples/basic.rs b/examples/basic.rs
index f4f7a24..4bd0b93 100644
--- a/examples/basic.rs
+++ b/examples/basic.rs
@@ -42,17 +42,21 @@ fn opts() -> OptionParser<Out> {
     let output = short('o')
         .long("output")
         .help("output file")
-        .argument::<PathBuf>("OUTPUT"); // but it's optional when rustc can derive it
+        .argument::<PathBuf>("OUTPUT") // but it's optional when rustc can derive it
+        .complete_shell(ShellComp::File { mask: None });
 
     // no magical name transmogrifications in combinatoric API,
     let nb_cars = short('n').long("nb-cars").argument::<u32>("N");
 
     // a parser that consumes one argument
     // you can build the inner parser in one go or as multiple steps giving each step a name
+    // you can also add some static shell completion functionality
     let file_to_proces = short('f')
         .long("file")
         .help("File to process")
-        .argument::<PathBuf>("FILE");
+        .argument::<PathBuf>("FILE")
+        .complete_shell(ShellComp::File { mask: Some("*.rs") });
+
     let files_to_process = file_to_proces.many();
 
     // packing things in a struct assumes parser for each field is in scope.
diff --git a/examples/derive_this_or_that.rs b/examples/derive_this_or_that.rs
index 4409bcd..d75dc2f 100644
--- a/examples/derive_this_or_that.rs
+++ b/examples/derive_this_or_that.rs
@@ -6,10 +6,7 @@ use bpaf::Bpaf;
 
 // By default bpaf tries to parse booleans as flags, do something smart
 // about Strings and file names and handles Option/Vec.
-// Everything else is handled as a textual named argument. In this case
-// we want to use external (with respect to Opts) function `flags`
-// derived for `Flags`. Overall it looks like this:
-
+// Everything else is handled as a textual named argument.
 #[derive(Debug, Clone, Bpaf)]
 #[bpaf(options)]
 enum Opts {
@@ -18,16 +15,10 @@ enum Opts {
     /// disabled
     Off,
     /// undecined
-    #[bpaf(long("undecided"), hide, default)]
+    #[bpaf(hide, default)]
     Undecided,
 }
 
-#[derive(Debug, Clone, Bpaf)]
-enum Flags {
-    This,
-    That,
-}
-
 fn main() {
     println!("{:?}", opts().run())
 }
diff --git a/examples/shared_args.rs b/examples/shared_args.rs
new file mode 100644
index 0000000..ccc4bf7
--- /dev/null
+++ b/examples/shared_args.rs
@@ -0,0 +1,40 @@
+//! It is possible to have shared args returned alongside a subcommand
+
+use bpaf::*;
+
+#[derive(Debug, Clone, Bpaf)]
+#[allow(dead_code)]
+struct Action {
+    verbose: bool,
+    number: u32,
+}
+
+#[derive(Debug, Clone, Bpaf)]
+#[allow(dead_code)]
+struct Build {
+    verbose: bool,
+}
+
+#[derive(Debug, Clone)]
+enum Command {
+    Action(Action),
+    Build(Build),
+}
+
+fn shared() -> impl Parser<Vec<String>> {
+    positional("ARG").many()
+}
+
+fn parse_command() -> impl Parser<(Command, Vec<String>)> {
+    let action = action().map(Command::Action);
+    let action = construct!(action, shared()).to_options().command("action");
+    let build = build().map(Command::Build);
+    let build = construct!(build, shared()).to_options().command("build");
+    construct!([action, build])
+}
+
+fn main() {
+    let opts = parse_command().to_options().run();
+
+    println!("{:?}", opts);
+}
diff --git a/src/complete_gen.rs b/src/complete_gen.rs
index caaafb2..f667f03 100644
--- a/src/complete_gen.rs
+++ b/src/complete_gen.rs
@@ -14,7 +14,13 @@
 //
 // complete short names to long names if possible
 
-use crate::{args::Arg, item::ShortLong, parsers::NamedArg, Args, CompleteDecor, Error};
+use crate::{
+    args::Arg,
+    complete_shell::{write_shell, Shell},
+    item::ShortLong,
+    parsers::NamedArg,
+    Args, CompleteDecor, Error, ShellComp,
+};
 use std::ffi::OsStr;
 
 #[derive(Clone, Debug)]
@@ -139,6 +145,19 @@ impl Arg {
 }
 
 impl Complete {
+    pub(crate) fn push_shell(&mut self, op: ShellComp, depth: usize, is_arg: bool) {
+        self.comps.push(Comp::Shell {
+            extra: CompExtra {
+                depth,
+                hidden_group: "",
+                visible_group: "",
+                help: None,
+            },
+            script: op,
+            is_arg,
+        });
+    }
+
     pub(crate) fn push_value(
         &mut self,
         body: String,
@@ -206,6 +225,11 @@ pub(crate) enum Comp {
         meta: &'static str,
         is_arg: bool,
     },
+    Shell {
+        extra: CompExtra,
+        script: ShellComp,
+        is_arg: bool,
+    },
 }
 
 impl Comp {
@@ -216,6 +240,7 @@ impl Comp {
             | Comp::Value { extra, .. }
             | Comp::Positional { extra, .. }
             | Comp::Flag { extra, .. }
+            | Comp::Shell { extra, .. }
             | Comp::Argument { extra, .. } => extra.depth,
         }
     }
@@ -228,6 +253,7 @@ impl Comp {
             Comp::Command { .. }
             | Comp::Value { .. }
             | Comp::Flag { .. }
+            | Comp::Shell { .. }
             | Comp::Argument { .. } => None,
             Comp::Positional { is_arg, .. } => Some(*is_arg),
         }
@@ -239,6 +265,7 @@ impl Comp {
             | Comp::Argument { extra, .. }
             | Comp::Command { extra, .. }
             | Comp::Value { extra, .. }
+            | Comp::Shell { extra, .. }
             | Comp::Positional { extra, .. } => extra,
         };
         match style {
@@ -387,6 +414,7 @@ impl Complete {
         pos_only: bool,
     ) -> Result<String, std::fmt::Error> {
         let mut items: Vec<ShowComp> = Vec::new();
+        let mut shell = Vec::new();
         let max_depth = self.comps.iter().map(Comp::depth).max().unwrap_or(0);
         let mut has_values = false;
 
@@ -487,6 +515,10 @@ impl Complete {
                         is_value: false,
                     });
                 }
+                Comp::Shell { script, is_arg, .. } => {
+                    has_values |= is_arg;
+                    shell.push(*script);
+                }
             }
         }
 
@@ -494,8 +526,18 @@ impl Complete {
             items.retain(|i| i.is_value);
         }
         match self.output_rev {
-            1 => render_1(&items),
-            2 => render_2(&items),
+            1 => {
+                assert!(shell.is_empty(), "You need to regenerate your completion scripts");
+                render_1(&items)
+            }
+            2 => {
+                assert!(shell.is_empty(), "You need to regenerate your completion scripts");
+                render_2(&items)
+            }
+            3 => render_3456(&items, Shell::Bash, &shell),
+            4 => render_3456(&items, Shell::Zsh, &shell),
+            5 => render_3456(&items, Shell::Fish,&shell),
+            6 => render_3456(&items, Shell::Elvish, &shell),
             unk => panic!("Unsupported output revision {}, you need to genenerate your shell completion files for the app", unk)
         }
     }
@@ -550,6 +592,45 @@ fn render_2(items: &[ShowComp]) -> Result<String, std::fmt::Error> {
     Ok(res)
 }
 
+fn render_3456(
+    items: &[ShowComp],
+    shell: Shell,
+    ops: &[ShellComp],
+) -> Result<String, std::fmt::Error> {
+    use std::fmt::Write;
+    let mut res = String::new();
+    if items.len() == 1 && ops.is_empty() {
+        write!(res, "literal\t{}", items[0].subst)?;
+        return Ok(res);
+    }
+
+    for i in items {
+        write!(res, "literal\t{}\tshow\t{}", i.subst, i.pretty)?;
+        if let Some(h) = &i.extra.help {
+            write!(res, "    {}", h.split('\n').next().unwrap_or(""))?;
+        }
+
+        if !i.extra.visible_group.is_empty() {
+            write!(res, "\tvis_group\t{}", i.extra.visible_group)?;
+        }
+
+        if i.extra.hidden_group.is_empty() {
+            if !i.extra.visible_group.is_empty() {
+                write!(res, "\thid_group\t{}", i.extra.visible_group)?;
+            }
+        } else {
+            write!(res, "\thid_group\t{}", i.extra.hidden_group)?;
+        }
+        writeln!(res)?;
+    }
+
+    for op in ops {
+        write_shell(&mut res, shell, *op)?;
+    }
+
+    Ok(res)
+}
+
 // to allow using ? inside check_complete
 impl From<std::fmt::Error> for Error {
     fn from(_: std::fmt::Error) -> Self {
diff --git a/src/complete_run.rs b/src/complete_run.rs
index 3a334b4..a1a9d31 100644
--- a/src/complete_run.rs
+++ b/src/complete_run.rs
@@ -1,3 +1,43 @@
+//! # Autocomplete protocol
+//!
+//! ## Version 1
+//! Goals: something simple to get it working in bash and other shells
+//! without adding complexity
+//!
+//! One item per line, \t separated sections.
+//! If there's only one possible completion - only replacement itself is inserted
+//! One item per line
+//! - item to insert
+//! - item description, op
+//!
+//! ## Version 2
+//! Goals: extended version of version 1 to allow group breakdown in zsh
+//!
+//! One item per line, \0 separated sections
+//! - item to insert
+//! - item description
+//! - visible group
+//! - hidden group
+
+//! ## Versions 3/4/5/6
+//! Goals: something to allow extending protocol to support custom command like "complete file"
+//!
+//! One item per line, \t separated keys and values:
+//! <KEY>\t<VAL>\t<KEY>\t<VAL>...<KEY>\t<VAL>
+//!
+//! For dynamic completion first key is always "literal"
+//! - `"literal"` - literal value to insert
+//! - `"show"` - value to display, can include metavars and such
+//! - `"vis_group"` - visible group
+//! - `"hid_group"` - hidden group
+//!
+//! For shell completion possible keys are
+//! - `"bash"` - rendered by version 3
+//! - `"zsh"` - version 4
+//! - `"fish"` - version 5
+//! - `"elvish"` - version 6
+//! and should be followed by a single value - code for shell to evaluate
+
 use std::{ffi::OsString, path::PathBuf};
 
 use crate::{construct, Args};
@@ -13,24 +53,42 @@ pub enum Style {
 fn dump_bash_completer(name: &str) {
     println!(
         "\
-#/usr/bin/env bash
 _bpaf_dynamic_completion()
 {{
+    _init_completion || return
+    local kw;
+
     COMPREPLY=()
 
     IFS=$'\\n' BPAF_REPLY=($( \"$1\" --bpaf-complete-rev={rev} \"${{COMP_WORDS[@]:1}}\" ))
-    for line in ${{BPAF_REPLY[@]}} ; do
+    for line in \"${{BPAF_REPLY[@]}}\" ; do
         IFS=$'\\t' parts=( $line )
-        if [[ -n ${{parts[1]}} ]] ; then
-            COMPREPLY+=($( printf \"%-19s %s\" \"${{parts[0]}}\" \"${{parts[1]}}\" ))
+        declare -A table;
+        if [[ \"${{parts[0]}}\" == \"literal\" ]] ; then
+            kw=\"\"
+            for part in \"${{parts[@]}}\" ; do
+                if [ -z \"$kw\" ] ; then
+                    kw=\"$part\"
+                else
+                    table[\"$kw\"]=\"$part\"
+                    kw=\"\"
+                fi
+            done
+            if [ ${{table[\"show\"]+x}} ] ; then
+                COMPREPLY+=(\"${{table[\"show\"]}}\")
+            else
+                COMPREPLY+=(\"${{table[\"literal\"]}}\")
+            fi
+        elif [[ \"${{parts[0]}}\" == \"bash\" ]] ; then
+            ${{parts[1]}} \"${{parts[@]:2}}\"
         else
-            COMPREPLY+=(${{parts[0]}})
+            COMPREPLY+=(\"${{parts[0]}}\")
         fi
     done
 }}
 complete -F _bpaf_dynamic_completion {name}",
         name = name,
-        rev = 1,
+        rev = 3
     );
 }
 
@@ -38,24 +96,38 @@ fn dump_zsh_completer(name: &str) {
     println!(
         "\
 #compdef {name}
+
 IFS=$'\\n' lines=($( \"${{words[1]}}\" --bpaf-complete-rev={rev} \"${{words[@]:1}}\" ))
+
 for line in \"${{(@)lines}}\" ; do
-    IFS=$'\\0' parts=($( echo $line ))
-    cmd=()
-    if [[ -n $parts[2] ]] ; then
-        descr=($parts[2])
-        cmd+=(-d descr)
-    fi
-    if [[ -n $parts[3] ]] ; then
-        cmd+=(-X \"${{parts[3]}}\")
-    fi
-    if [[ -n $parts[4] ]] ; then
-        cmd+=(-J \"${{parts[4]}}\")
+    IFS=$'\\t' parts=( $(echo \"$line\") )
+    if [[ \"${{parts[1]}}\" == \"literal\" ]] ; then
+        typeset -A table
+        IFS=$'\\t' table=( $(echo -e \"$line\") )
+
+        show=( $table[show] )
+        if [[ ${{#table[@]}} -ne 0 ]] ; then
+            cmd+=(-d show)
+        fi
+
+        if [[ -n $table[vis_group] ]] ; then
+            cmd+=(-X $table[vis_group])
+        fi
+
+        if [[ -n $table[hid_group] ]] ; then
+            cmd+=(-J $table[vis_group])
+        fi
+
+        compadd ${{cmd[@]}} -- $table[literal]
+    elif [[ \"${{parts[1]}}\" == \"zsh\" ]] ; then
+        ${{parts[2]}}
+    else
+        compadd -- \"${{parts[1]}}\"
     fi
-    compadd ${{cmd[@]}} -- \"$parts[1]\"
+
 done",
         name = name,
-        rev = 2,
+        rev = 4,
     );
 }
 
diff --git a/src/complete_shell.rs b/src/complete_shell.rs
new file mode 100644
index 0000000..58d96b5
--- /dev/null
+++ b/src/complete_shell.rs
@@ -0,0 +1,146 @@
+use crate::{Args, Error, Meta, Parser};
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum Shell {
+    Bash,
+    Zsh,
+    Fish,
+    Elvish,
+}
+
+#[derive(Debug, Clone, Copy)]
+/// Shell specific completion
+#[non_exhaustive]
+pub enum ShellComp {
+    /// A file or directory name with an optional file mask.
+    ///
+    /// For bash filemask should start with `*.` or contain only the
+    /// extension
+    File {
+        /// Optional filemask to use, no spaces, no tabs
+        mask: Option<&'static str>,
+    },
+
+    /// Similar to `File` but limited to directories only
+    /// For bash filemask should start with `*.` or contain only the
+    /// extension
+    Dir {
+        /// Optional filemask to use, no spaces, no tabs
+        mask: Option<&'static str>,
+    },
+
+    /// You can also specify a raw value to use for each supported shell
+    ///
+    /// It is possible to fill in values for shells you don't want to support
+    /// with empty strings but the code is not going to work for those shells
+    Raw {
+        /// This raw string will be used for `bash` shell
+        /// <https://www.gnu.org/software/bash/manual/html_node/Command-Line-Editing.html>
+        bash: &'static str,
+
+        /// This raw string will be used for `zsh` shell
+        /// <https://zsh.sourceforge.io/Doc/Release/Completion-System.html>
+        zsh: &'static str,
+
+        /// This raw string will be used for `fish` shell
+        /// <https://fishshell.com/docs/current/completions.html>
+        fish: &'static str,
+
+        /// This raw string will be used for `elvish` shell
+        /// <https://elv.sh/ref/edit.html#completion-api>
+        elvish: &'static str,
+    },
+
+    /// Don't produce anything at all from this parser - can be useful if you want to compose
+    /// bpaf completion with shell completion
+    Nothing,
+}
+
+/// Parser that inserts static shell completion into bpaf's dynamic shell completion
+#[cfg(feature = "autocomplete")]
+pub struct ParseCompShell<P> {
+    pub(crate) inner: P,
+    pub(crate) op: crate::complete_shell::ShellComp,
+}
+
+#[cfg(feature = "autocomplete")]
+impl<P, T> Parser<T> for ParseCompShell<P>
+where
+    P: Parser<T> + Sized,
+{
+    fn eval(&self, args: &mut Args) -> Result<T, Error> {
+        // same as with ParseComp the goal is to replace metavars added by inner parser
+        // with a completion that would call a bash script.
+        // unlike ParseComp we don't care if inner parser succeeds
+
+        // stash old completions
+        let mut comp_items = Vec::new();
+        args.swap_comps(&mut comp_items);
+
+        let res = self.inner.eval(args);
+
+        // at this point comp_items contains values added by the inner parser
+        args.swap_comps(&mut comp_items);
+
+        if let Some(comp) = &mut args.comp {
+            for ci in comp_items {
+                if let Some(is_arg) = ci.meta_type() {
+                    comp.push_shell(self.op, args.depth, is_arg);
+                } else {
+                    comp.comps.push(ci);
+                }
+            }
+        }
+
+        res
+    }
+
+    fn meta(&self) -> Meta {
+        self.inner.meta()
+    }
+}
+
+pub(crate) fn write_shell(res: &mut String, shell: Shell, comp: ShellComp) -> std::fmt::Result {
+    match shell {
+        Shell::Bash => write_bash(res, comp),
+        Shell::Zsh => write_zsh(res, comp),
+        // not supported at the moment
+        Shell::Fish | Shell::Elvish => Ok(()),
+    }
+}
+
+fn write_bash(res: &mut String, comp: ShellComp) -> std::fmt::Result {
+    use std::fmt::Write;
+    match comp {
+        ShellComp::File { mask: None } => write!(res, "bash\t_filedir"),
+        ShellComp::File { mask: Some(mask) } => {
+            writeln!(res, "bash\t_filedir '{}'", bashmask(mask))
+        }
+        ShellComp::Dir { mask: None } => write!(res, "bash\t_filedir -d"),
+        ShellComp::Dir { mask: Some(mask) } => {
+            writeln!(res, "bash\t_filedir -d '{}'", bashmask(mask))
+        }
+        ShellComp::Raw { bash, .. } => writeln!(res, "bash\t{}", bash),
+        ShellComp::Nothing => Ok(()),
+    }
+}
+
+// Bash is strange when it comes to completion - rather than taking
+// a glob - _filedir takes an extension which it later to include uppercase
+// version as well and to include "*." in front. For compatibility with
+// zsh and other shells - this code strips "*." from the beginning....
+fn bashmask(i: &str) -> &str {
+    i.strip_prefix("*.").unwrap_or(i)
+}
+
+fn write_zsh(res: &mut String, comp: ShellComp) -> std::fmt::Result {
+    use std::fmt::Write;
+    match comp {
+        ShellComp::File { mask: None } => writeln!(res, "zsh\t_files"),
+        ShellComp::File { mask: Some(mask) } => writeln!(res, "zsh\t_files -g '{}'", mask),
+        ShellComp::Dir { mask: None } => writeln!(res, "zsh\t_files -/"),
+        ShellComp::Dir { mask: Some(mask) } => writeln!(res, "zsh\t_files -/ -g '{}'", mask),
+        ShellComp::Raw { zsh, .. } => writeln!(res, "zsh\t{}", zsh),
+        ShellComp::Nothing => Ok(()),
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 60503d2..9641062 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,6 @@
 #![warn(missing_docs)]
 #![allow(clippy::needless_doctest_main)]
+#![allow(clippy::redundant_else)] // not useful
 
 //! Lightweight and flexible command line argument parser with derive and combinatoric style API
 
@@ -391,6 +392,8 @@ pub mod batteries;
 mod complete_gen;
 #[cfg(feature = "autocomplete")]
 mod complete_run;
+#[cfg(feature = "autocomplete")]
+mod complete_shell;
 mod info;
 mod item;
 mod meta;
@@ -409,11 +412,16 @@ use std::marker::PhantomData;
 #[doc(hidden)]
 pub use structs::{ParseBox, ParseCon};
 
+#[cfg(feature = "autocomplete")]
+pub use crate::complete_shell::ShellComp;
+
 pub mod parsers {
     //! This module exposes parsers that accept further configuration with builder pattern
     //!
     //! In most cases you won't be using those names directly, they're only listed here to provide
     //! access to documentation
+    #[cfg(feature = "autocomplete")]
+    pub use crate::complete_shell::ParseCompShell;
     pub use crate::params::{NamedArg, ParseArgument, ParseCommand, ParsePositional};
     pub use crate::structs::{ParseBox, ParseMany, ParseOptional, ParseSome};
 }
@@ -1236,6 +1244,58 @@ pub trait Parser<T> {
     }
     // }}}
 
+    // {{{
+    /// Static shell completion
+    ///
+    /// Allows to ask existing shell completion to provide some information such as file or
+    /// directory names or pass though existing shell completion scripts, see
+    /// [`ShellComp`](complete_shell::ShellComp) for accessible functionality
+    ///
+    /// Places function call in place of metavar placeholder, so running `complete_shell` on
+    /// something that doesn't have a [`positional`] or [`argument`](NamedArg::argument) doesn't
+    /// make much sense.
+    ///
+    /// **Using this function requires enabling `"autocomplete"` feature, not enabled by default**.
+    ///
+    /// # Example
+    /// ```console
+    /// $ app --output C<TAB>
+    /// $ app --output Cargo.toml _
+    /// ```
+    ///
+    /// # Combinatoric usage
+    /// ```rust
+    /// # use bpaf::*;
+    /// fn output() -> impl Parser<String> {
+    ///     long("output")
+    ///         .help("Cargo.toml file to use as output")
+    ///         .argument("OUTPUT")
+    ///         .complete_shell(ShellComp::File { mask: Some("*.toml") })
+    /// }
+    /// ```
+    ///
+    /// # Derive usage
+    /// ```rust
+    /// # use bpaf::*;
+    /// #[derive(Debug, Clone, Bpaf)]
+    /// struct Options {
+    ///     /// Cargo.toml file to use as output
+    ///     #[bpaf(argument("OUTPUT"), complete_shell(ShellComp::File { mask: Some("*.toml") }))]
+    ///     output: String,
+    /// }
+    /// ```
+    #[cfg(feature = "autocomplete")]
+    fn complete_shell(
+        self,
+        op: complete_shell::ShellComp,
+    ) -> crate::complete_shell::ParseCompShell<Self>
+    where
+        Self: Sized + Parser<T>,
+    {
+        crate::complete_shell::ParseCompShell { inner: self, op }
+    }
+    // }}}
+
     // {{{ complete_style
     /// Add extra annotations to completion information
     ///
@@ -1442,6 +1502,7 @@ pub trait Parser<T> {
 pub enum CompleteDecor {
     /// Group items according to this group
     HiddenGroup(&'static str),
+
     /// Group items according to this group but also show the group name
     VisibleGroup(&'static str),
 }
diff --git a/src/structs.rs b/src/structs.rs
index 59b33f1..3ababa4 100644
--- a/src/structs.rs
+++ b/src/structs.rs
@@ -609,13 +609,13 @@ where
     fn eval(&self, args: &mut Args) -> Result<T, Error> {
         // stash old
         let mut comp_items = Vec::new();
-
         args.swap_comps(&mut comp_items);
 
         let res = self.inner.eval(args);
 
         // restore old, now metavars added by inner parser, if any, are in comp_items
         args.swap_comps(&mut comp_items);
+
         if let Some(comp) = &mut args.comp {
             if res.is_err() {
                 comp.comps.extend(comp_items);
@@ -625,6 +625,8 @@ where
 
         let res = res?;
 
+        // completion function generates suggestions based on the parsed inner value, for
+        // that `res` must contain a parsed value
         if let Some(comp) = &mut args.comp {
             for ci in comp_items {
                 if let Some(is_arg) = ci.meta_type() {
@@ -648,6 +650,7 @@ where
         self.inner.meta()
     }
 }
+
 #[cfg(feature = "autocomplete")]
 pub struct ParseCompStyle<P> {
     pub(crate) inner: P,
@@ -758,6 +761,9 @@ where
     P: Parser<T> + Sized,
 {
     fn eval(&self, args: &mut Args) -> Result<T, Error> {
+        #[cfg(feature = "autocomplete")]
+        let mut comp_items = Vec::new();
+
         for (start, mut this_arg) in args.ranges() {
             if let Ok(ok) = self.inner.eval(&mut this_arg) {
                 if start > 0 {
@@ -765,8 +771,14 @@ where
                 }
                 std::mem::swap(&mut this_arg, args);
                 return Ok(ok);
+            } else {
+                #[cfg(feature = "autocomplete")]
+                this_arg.swap_comps(&mut comp_items);
             }
         }
+
+        #[cfg(feature = "autocomplete")]
+        args.swap_comps(&mut comp_items);
         Err(Error::Missing(vec![]))
     }
 


Test patch:
diff --git a/bpaf_derive/src/field_tests.rs b/bpaf_derive/src/field_tests.rs
index b715946..2f5e70a 100644
--- a/bpaf_derive/src/field_tests.rs
+++ b/bpaf_derive/src/field_tests.rs
@@ -421,6 +421,21 @@ fn optional_argument_with_name_complete() {
     assert_eq!(input.to_token_stream().to_string(), output.to_string());
 }
 
+#[test]
+fn optional_argument_with_name_shell_complete() {
+    let input: NamedField = parse_quote! {
+        #[bpaf(argument("N"), complete_shell(magic))]
+        config: Option<u64>
+    };
+    let output = quote! {
+        ::bpaf::long("config")
+            .argument::<u64>("N")
+            .optional()
+            .complete_shell(magic)
+    };
+    assert_eq!(input.to_token_stream().to_string(), output.to_string());
+}
+
 #[test]
 fn some_arguments() {
     let input: NamedField = parse_quote! {
diff --git a/src/tests.rs b/src/tests.rs
index 781847e..2b9f350 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -75,7 +75,7 @@ fn wtf_shenanigans_2() {
 }
 
 #[test]
-fn fallback_with_strange_args() {
+fn fallback_with_strange_args_produces_same_results() {
     use crate::args::{split_os_argument, split_os_argument_fallback};
     let s = std::ffi::OsString::from("-Obits=2048");
     let r1 = split_os_argument(&s);
diff --git a/tests/completion.rs b/tests/completion.rs
index 1e27377..4d48eda 100644
--- a/tests/completion.rs
+++ b/tests/completion.rs
@@ -1081,3 +1081,41 @@ fn avoid_inserting_metavars() {
     test_zsh_comp(&parser, &["-a", ""], &[["-a", "-a <A>", "", ""]]);
     test_zsh_comp(&parser, &[""], &[["-a", "-a <A>", "", ""]]);
 }
+
+#[track_caller]
+fn test_comp_v3<T: std::fmt::Debug>(
+    parser: &OptionParser<T>,
+    args: &[&str],
+    expected: &[&str],
+    rev: usize,
+) {
+    let comps = parser
+        .run_inner(Args::from(args).set_comp(rev))
+        .unwrap_err()
+        .unwrap_stdout();
+    let mut total = 0;
+    for (ix, comp) in comps.lines().enumerate() {
+        total += 1;
+        if let Some(exp) = expected.get(ix) {
+            assert_eq!(*exp, comp);
+        } else {
+            panic!("Got more comps than expected: {:?}", comp);
+        }
+    }
+    assert_eq!(total, expected.len(), "expected more than got");
+}
+
+#[test]
+fn bash_completion() {
+    let parser = short('a')
+        .argument::<String>("FILE")
+        .complete_shell(ShellComp::Dir { mask: None })
+        .to_options();
+
+    test_comp_v3(&parser, &["-a", ""], &["bash\t_filedir -d"], 3);
+    test_comp_v3(&parser, &["-a", ""], &["zsh\t_files -/"], 4);
+
+    // not yet fully supported
+    test_comp_v3(&parser, &["-a", ""], &[], 5);
+    test_comp_v3(&parser, &["-a", ""], &[], 6);
+}