Repository: pacak/bpaf
Pull request #84 (2022-09-29 14:12:04)

cauwugo - dynamic shel completion infused cargo
limited commands for now but seem to work


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 7baf3e8..6aa4415 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -49,3 +49,6 @@ required-features = ["autocomplete"]
 
 [workspace]
 members = [".", "./bpaf_derive", "./bpaf_cauwugo", "docs"]
+
+[workspace.metadata.cauwugo]
+bpaf = true
diff --git a/bpaf_cauwugo/Cargo.toml b/bpaf_cauwugo/Cargo.toml
index 6a03e2c..8f534f2 100644
--- a/bpaf_cauwugo/Cargo.toml
+++ b/bpaf_cauwugo/Cargo.toml
@@ -2,6 +2,12 @@
 name = "bpaf_cauwugo"
 version = "0.1.0"
 edition = "2021"
+license = "MIT OR Apache-2.0"
+categories = ["command-line-interface", "command-line-utilities"]
+repository = "https://github.com/pacak/bpaf"
+keywords = ["cargo", "cargo-frontend", "cli", "completion", "dynamic-completion"]
+readme = "README.md"
+description = "A cargo frontend with dynamic completion"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
diff --git a/bpaf_cauwugo/README.md b/bpaf_cauwugo/README.md
new file mode 100644
index 0000000..5c3878c
--- /dev/null
+++ b/bpaf_cauwugo/README.md
@@ -0,0 +1,131 @@
+# Dynamic completion enhanced cargo
+
+## Completions supported by `cargo` itself
+
+Command line completion is a common feature implemented in shells or other places of text mode
+communication with a computer in which program automatically or on demand fills in partially
+typed programs. Completion can be static or dynamic. For static completion set of possible
+continuations is mostly fixed, but can include things like file names or parts of it. For
+dynamic completions set of possible continuations is computed on runtime by a shell function or
+a program.
+
+Cargo comes with static completion for `zsh` you can find its contents by typing
+```console
+% cat "$(rustc --print sysroot)"/share/zsh/site-functions/_cargo
+```
+This completion fills in commands and flag names, examples (not always correct) or tests.
+
+## Dynamic completions in `cauwugo`
+
+`cauwugo` uses [bpaf](https://crates.io/crates/bpaf) to provide fully dynamic shell completions
+for all `bash`, `zsh`, `fish` and `elvish` and helps to fill in not only flag names but possible
+values as well.
+
+Currently supported commands are: `add`, `build`, `check`, `clean`, `test` and `run`.
+
+## `cauwugo add`
+
+Currently searches for available packages (`cargo search`) and completes them and completes which
+workspace member to add it to, if any. It should be possible to support dealing with features as
+well, but that's in TODO.
+
+```console
+% cauwugo add serde<TAB>
+serde    A generic serialization/deserialization framework
+serde_alias    An attribute macro to apply serde aliases to all struct fields
+serde_amqp    A serde implementation of AMQP1.0 protocol.
+serde_any    Dynamic serialization and deserialization with the format chosen at runtime
+serde_asn1_der    A basic ASN.1-DER implementation for `serde` based upon `asn1_der`
+serde-big-array    Big array helper for serde.
+[skip]
+% cauwugo add serde_j
+% cauwugo add serde_j<TAB>
+% cauwugo add serde_json -p <TAB>
+bpaf          bpaf_cauwugo  bpaf_derive   docs
+% cauwugo add serde_json -p
+% cauwugo add serde_json -p <TAB><TAB>
+% cauwugo add serde_json -p bpaf_cauwugo
+```
+
+## `cauwugo build`
+
+Completes binary/executable/bench/test/package names plus currently installed targets:
+
+```console
+% cauwugo build --example se<TAB>
+% cauwugo build --example sensors
+% cauwugo build --example sensros --target <TAB>
+% cauwugo build --example sensors --target
+armv7-unknown-linux-gnueabihf  x86_64-pc-windows-gnu          x86_64-unknown-linux-gnu
+% cauwugo build --example sensors --target a<TAB>
+% cauwugo build --example sensors --target armv7-unknown-linux-gnueabihf
+```
+
+## `cauwugo check`
+
+Similar to `cauwugo build` completes binary/executable/bench/test/package names + target
+
+## `cauwugo clean`
+
+Completes package names
+
+```
+% cauwugo clean --p<TAB>
+% cauwugo clean --package
+% cauwugo clean --package d<TAB>
+% cauwugo clean --package docs
+```
+
+## `cauwugo test`
+
+Completes usual names for testable things, but also supports brief notation that includes
+actual test names so you can run just one test: `cargo test TESTABLE TEST_NAME`.
+
+```console
+% cauwugo test <TAB>
+[skip]
+Bins, tests, examples
+adjacent    bpaf    bpaf_derive    derive    help_format
+% cauwugo test de<TAB>
+% cauwugo test derive
+% cauwugo test derive <TAB>
+Available test names
+command_and_fallback    help_with_default_parse
+% cauwugo test derive co<TAB>
+% cauwugo test derive command_and_fallback<ENTER>
+    Finished test [unoptimized + debuginfo] target(s) in 0.00s
+     Running tests/derive.rs (target/debug/deps/derive-7ef117342dbf6a84)
+
+running 1 test
+test command_and_fallback ... ok
+
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
+```
+
+## `cauwugo run`
+
+Completes a usual set of runnable item names plus target, but also supports completion
+passthough to runnable items themselves, so if crates in your workspace use `bpaf` with dynamic
+completion enabled you can do something like this, where anything after `--` comes from
+`sensors` example itself and device names - from dynamic completion running inside it.
+
+```
+% cauwugo run se<TAB>
+% cauwugo run sensors
+% cauwugo run sensors -- <TAB>
+% cauwugo run sensors -- --sensor
+--sensor    --sensor-device <DEVICE>    --sensor-i2c-address <ADDRESS>    --sensor-name <NAME>
+% cauwugo run sensors -- --sensor --sensor-device <TAB><TAB>
+elakelaiset% cauwugo run sensors -- --sensor-device outdoor                                                                                                                                                                         ~/ej/bpaf
+outdoor    Outdoor temperature sensor
+tank01    Temperature in a storage tank 1
+tank02    Temperature in a storage tank 2
+tank03    Temperature in a storage tank 3
+temp100    Main temperature sensor
+temp101    Output temperature sensor
+```
+
+Both here and in `cauwugo test` `cauwugo` compiles tests and examples and asks them for
+possible values directly. You don't need to configure anything else other than completion
+configuration for `cauwugo` itself to use this functionality. This probably won't work if you
+are doing crosscompilation
diff --git a/bpaf_cauwugo/src/add.rs b/bpaf_cauwugo/src/add.rs
new file mode 100644
index 0000000..42899b3
--- /dev/null
+++ b/bpaf_cauwugo/src/add.rs
@@ -0,0 +1,115 @@
+use crate::shared::parse_package;
+use bpaf::*;
+use std::{path::PathBuf, process::Command};
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(command, generate(parse_add))]
+/// Add dependencies to a Cargo.toml manifest file
+pub struct Add {
+    #[bpaf(external)]
+    pub package: Option<&'static str>,
+
+    /// Add as dev dependency
+    pub dev: bool,
+
+    /// Add as build dependency
+    pub build: bool,
+
+    #[bpaf(external)]
+    pub source: Source,
+}
+
+#[derive(Debug, Clone, Bpaf)]
+pub enum Source {
+    Git {
+        #[bpaf(long, argument("URI"))]
+        /// Git repository location
+        git: String,
+
+        #[bpaf(long, argument("BRANCH"))]
+        /// Git branch to download the crate from
+        branch: Option<String>,
+
+        #[bpaf(long, argument("TAG"))]
+        /// Git tag to download the crate from
+        tag: Option<String>,
+
+        #[bpaf(long, argument("TAG"))]
+        /// Git reference to download the crate from
+        rev: Option<String>,
+    },
+    Local {
+        /// Filesystem path to local crate to add
+        path: PathBuf,
+    },
+    Crates {
+        /// Package registry to use instead of crates.io
+        registry: Option<String>,
+
+        /// Package name
+        #[bpaf(positional("DEP"), complete(complete_available_package))]
+        name: String,
+    },
+}
+
+/// ask cargo search for all the available packages matching the name
+///
+/// TODO: do some filtering - squatted names and such don't belong here
+fn complete_available_package(name: &String) -> Vec<(String, Option<String>)> {
+    if name.is_empty() {
+        return vec![("<PACKAGE>".to_string(), None)];
+    }
+    let mut cmd = Command::new("cargo");
+    cmd.args(["search", "--limit", "100"]); // greedy cargo will print only 100 or so at most :(
+    cmd.arg(name);
+    let output = cmd.output().expect("Couldn't run cargo search?");
+
+    if output.status.code() == Some(0) {
+        String::from_utf8_lossy(&output.stdout)
+            .lines()
+            .filter_map(|line| {
+                if !line.starts_with(name) {
+                    return None;
+                }
+                let (full_package, descr) = line.split_once(" # ")?;
+                let package = full_package.split(' ').next()?;
+                Some((package.to_owned(), Some(descr.to_owned())))
+            })
+            .collect::<Vec<_>>()
+    } else {
+        panic!("{:?}", output);
+    }
+}
+
+impl Add {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        cmd.arg("add");
+        pass_arg!(cmd, self.package, "--package");
+        pass_flag!(cmd, self.dev, "--dev");
+        pass_flag!(cmd, self.build, "--build");
+        match &self.source {
+            Source::Git {
+                git,
+                branch,
+                tag,
+                rev,
+            } => {
+                pass_req_arg!(cmd, git, "--git");
+                pass_arg!(cmd, branch, "--branch");
+                pass_arg!(cmd, tag, "--tag");
+                pass_arg!(cmd, rev, "--rev");
+            }
+            Source::Local { path } => {
+                pass_req_arg!(cmd, path, "--path");
+            }
+            Source::Crates { registry, name } => {
+                cmd.arg(name);
+                pass_arg!(cmd, registry, "--registry");
+            }
+        }
+    }
+}
+
+fn package() -> impl Parser<Option<&'static str>> {
+    parse_package("Package to add a new dependency to")
+}
diff --git a/bpaf_cauwugo/src/build.rs b/bpaf_cauwugo/src/build.rs
new file mode 100644
index 0000000..1ea418d
--- /dev/null
+++ b/bpaf_cauwugo/src/build.rs
@@ -0,0 +1,43 @@
+use crate::shared::{cargo_opts, package_and_testables, CargoOpts, PackageAndTestables};
+use bpaf::*;
+use std::process::Command;
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(command, short('b'), long("build"), generate(parse_build))]
+/// Compile a local package or parts of it and all of its dependencies
+pub struct Build {
+    #[bpaf(external)]
+    pub cargo_opts: CargoOpts,
+
+    /// Package to check
+    #[bpaf(external)]
+    pub package_and_testables: PackageAndTestables,
+
+    /// Build the library
+    pub lib: bool,
+
+    /// Build all the binaries
+    pub bins: bool,
+
+    /// Build all examples
+    pub examples: bool,
+
+    /// Build all tests
+    pub tests: bool,
+
+    /// Build all benchmarks
+    pub benches: bool,
+}
+
+impl Build {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        cmd.arg("build");
+        self.package_and_testables.pass_to_cmd(cmd);
+        self.cargo_opts.pass_to_cmd(cmd);
+        pass_flag!(cmd, self.lib, "--lib");
+        pass_flag!(cmd, self.bins, "--bins");
+        pass_flag!(cmd, self.examples, "--examples");
+        pass_flag!(cmd, self.benches, "--benches");
+        pass_flag!(cmd, self.tests, "--tests");
+    }
+}
diff --git a/bpaf_cauwugo/src/check.rs b/bpaf_cauwugo/src/check.rs
new file mode 100644
index 0000000..63f0c4d
--- /dev/null
+++ b/bpaf_cauwugo/src/check.rs
@@ -0,0 +1,43 @@
+use crate::shared::{cargo_opts, package_and_testables, CargoOpts, PackageAndTestables};
+use bpaf::*;
+use std::process::Command;
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(command, short('c'), long("check"), generate(parse_check))]
+/// Check a local package or parts of it and all of its dependencies for errors
+pub struct Check {
+    #[bpaf(external)]
+    pub cargo_opts: CargoOpts,
+
+    /// Package to check
+    #[bpaf(external)]
+    pub package_and_testables: PackageAndTestables,
+
+    /// Check the library
+    pub lib: bool,
+
+    /// Check all the binaries
+    pub bins: bool,
+
+    /// Check all examples
+    pub examples: bool,
+
+    /// Check all tests
+    pub tests: bool,
+
+    /// Check all benchmarks
+    pub benches: bool,
+}
+
+impl Check {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        cmd.arg("check");
+        self.package_and_testables.pass_to_cmd(cmd);
+        self.cargo_opts.pass_to_cmd(cmd);
+        pass_flag!(cmd, self.lib, "--lib");
+        pass_flag!(cmd, self.bins, "--bins");
+        pass_flag!(cmd, self.examples, "--examples");
+        pass_flag!(cmd, self.benches, "--benches");
+        pass_flag!(cmd, self.tests, "--tests");
+    }
+}
diff --git a/bpaf_cauwugo/src/clean.rs b/bpaf_cauwugo/src/clean.rs
new file mode 100644
index 0000000..c05e441
--- /dev/null
+++ b/bpaf_cauwugo/src/clean.rs
@@ -0,0 +1,27 @@
+use crate::shared::{cargo_opts, parse_package, CargoOpts};
+use bpaf::*;
+use std::process::Command;
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(command, generate(parse_clean))]
+/// Remove artifacts that cargo has generated in the past
+pub struct Clean {
+    #[bpaf(external)]
+    pub cargo_opts: CargoOpts,
+
+    /// Package to clean
+    #[bpaf(external)]
+    pub package: Option<&'static str>,
+}
+
+impl Clean {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        cmd.arg("clean");
+        pass_arg!(cmd, self.package, "--package");
+        self.cargo_opts.pass_to_cmd(cmd);
+    }
+}
+
+fn package() -> impl Parser<Option<&'static str>> {
+    parse_package("Package to clean artifacts for")
+}
diff --git a/bpaf_cauwugo/src/lib.rs b/bpaf_cauwugo/src/lib.rs
index 8cb1356..8bf0a9f 100644
--- a/bpaf_cauwugo/src/lib.rs
+++ b/bpaf_cauwugo/src/lib.rs
@@ -1,10 +1,73 @@
-pub mod opts;
+macro_rules! pass_flag {
+    ($cmd:ident, $field:expr, $value:literal) => {
+        if $field {
+            $cmd.arg($value);
+        }
+    };
+}
+
+macro_rules! pass_arg {
+    ($cmd:ident, $field:expr, $value:literal) => {
+        for v in $field {
+            $cmd.arg($value).arg(v);
+        }
+    };
+}
+
+macro_rules! pass_req_arg {
+    ($cmd:ident, $field:expr, $value:literal) => {
+        $cmd.arg($value).arg($field);
+    };
+}
+
+use bpaf::Parser;
+use std::{cell::RefCell, rc::Rc};
 
-#[cfg(test)]
-mod tests {
-    #[test]
-    fn it_works() {
-        let result = 2 + 2;
-        assert_eq!(result, 4);
+/// Stash successfully parsed value in a RefCell
+pub fn remember_opt<P, T>(parser: P, cell: &Rc<RefCell<T>>) -> impl Parser<T>
+where
+    P: Parser<T>,
+    T: Clone + 'static,
+{
+    let cell = cell.clone();
+    parser.map(move |val| {
+        *cell.borrow_mut() = val.clone();
+        val
+    })
+}
+
+/// Stash successfully parsed value in a RefCell.
+///
+/// Unlike [`remember_opt`] this one parses a required value
+pub fn remember_req<P, T>(parser: P, cell: &Rc<RefCell<Option<T>>>) -> impl Parser<T>
+where
+    P: Parser<T>,
+    T: Clone + 'static,
+{
+    let cell = cell.clone();
+    parser.map(move |val| {
+        *cell.borrow_mut() = Some(val.clone());
+        val
+    })
+}
+
+pub fn unique_match<I, T>(mut iter: I, name: &str) -> Result<T, String>
+where
+    I: Iterator<Item = T>,
+{
+    match (iter.next(), iter.next()) {
+        (None, _) => Err(format!("{} is not a known name", name)),
+        (Some(_), Some(_)) => Err(format!("{} is not a unique name", name)),
+        (Some(exec), None) => Ok(exec),
     }
 }
+
+pub mod add;
+pub mod build;
+pub mod check;
+pub mod clean;
+pub mod metadata;
+pub mod opts;
+pub mod run;
+pub mod shared;
+pub mod test;
diff --git a/bpaf_cauwugo/src/main.rs b/bpaf_cauwugo/src/main.rs
index 280e6d8..d623c34 100644
--- a/bpaf_cauwugo/src/main.rs
+++ b/bpaf_cauwugo/src/main.rs
@@ -1,22 +1,54 @@
-use bpaf_cauwugo::opts::{cargo_command, options, Options};
-
-fn main() {
-    let opts = options().run();
-
-    match opts {
-        Options::Run {
-            cargo_params,
-            args,
-            exec,
-        } => {
-            let mut cmd = cargo_command("run", &cargo_params, exec.as_ref());
-            let mut child = cmd.arg("--").args(&args).spawn().unwrap();
-            match child.wait().unwrap().code() {
-                Some(code) => std::process::exit(code),
-                None => {}
+use bpaf_cauwugo::opts::{cauwugo_opts, Cauwugo};
+use std::process::Command;
+
+fn main() -> std::io::Result<()> {
+    if let Some(first) = std::env::args_os().nth(1) {
+        let supported = first == "--help"
+            || first == "add"
+            || first == "build"
+            || first == "check"
+            || first == "clean"
+            || first == "run"
+            || first == "test"
+            || first.to_str().map_or(false, |s| s.starts_with("--bpaf"));
+
+        if !supported {
+            let mut cmd = Command::new("cargo");
+            cmd.args(std::env::args_os().skip(1));
+
+            let mut child = cmd.spawn()?;
+            if let Some(code) = child.wait()?.code() {
+                std::process::exit(code)
+            } else {
+                return Ok(());
             }
         }
     }
 
-    //    println!("{:?}", *EXECS);
+    let opts = cauwugo_opts().run();
+    let mut cmd = Command::new("cargo");
+    match opts.cauwugo {
+        Cauwugo::Add(add) => add.pass_to_cmd(&mut cmd),
+        Cauwugo::Build(build) => build.pass_to_cmd(&mut cmd),
+        Cauwugo::Check(check) => check.pass_to_cmd(&mut cmd),
+        Cauwugo::Clean(clean) => clean.pass_to_cmd(&mut cmd),
+        Cauwugo::Run(run) => run.pass_to_cmd(&mut cmd),
+        Cauwugo::Test(test) => test.pass_to_cmd(&mut cmd),
+        // bench       Run the benchmarks ?
+    }
+
+    if opts.bpaf_verbose {
+        println!("{:?}", cmd);
+    }
+
+    if opts.bpaf_dry {
+        return Ok(());
+    }
+
+    let mut child = cmd.spawn()?;
+    if let Some(code) = child.wait()?.code() {
+        std::process::exit(code)
+    }
+
+    Ok(())
 }
diff --git a/bpaf_cauwugo/src/metadata.rs b/bpaf_cauwugo/src/metadata.rs
new file mode 100644
index 0000000..2aa9159
--- /dev/null
+++ b/bpaf_cauwugo/src/metadata.rs
@@ -0,0 +1,184 @@
+use cargo_metadata::{CargoOpt, Metadata, MetadataCommand, Target};
+use once_cell::sync::Lazy;
+use std::process::Command;
+
+pub static METADATA: Lazy<Metadata> = Lazy::new(|| {
+    MetadataCommand::new()
+        .manifest_path("./Cargo.toml")
+        .features(CargoOpt::AllFeatures)
+        .no_deps()
+        .exec()
+        .unwrap()
+});
+
+pub fn bpaf_passthough_for(pkg: &str) -> bool {
+    fn get(pkg: &str) -> Option<bool> {
+        let meta = METADATA.workspace_metadata.get("cauwugo")?;
+        let bpaf = meta.get("bpaf")?;
+
+        if let Some(b) = bpaf.as_bool() {
+            Some(b)
+        } else if let Some(l) = bpaf.as_array() {
+            for enabled_for in l.iter().filter_map(|l| l.as_str()) {
+                if pkg == enabled_for {
+                    return Some(true);
+                }
+            }
+            Some(false)
+        } else {
+            Some(false)
+        }
+    }
+
+    get(pkg).unwrap_or(false)
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum MatchKind<'a> {
+    Prefix(&'a str),
+    Exact(&'a str),
+    Any,
+}
+
+impl<'a> MatchKind<'a> {
+    pub fn matches(&self, string: &str) -> bool {
+        match self {
+            MatchKind::Prefix(sel) => string.starts_with(sel),
+            MatchKind::Exact(sel) => sel == &string,
+            MatchKind::Any => true,
+        }
+    }
+
+    pub fn exact(string: Option<&'static str>) -> Self {
+        match string {
+            Some(s) => Self::Exact(s),
+            None => Self::Any,
+        }
+    }
+}
+
+/// all the matching targets using all the available information
+pub fn matching_targets<'a>(
+    package: MatchKind<'a>,
+    name: MatchKind<'a>,
+    kinds: &'static [&'static str],
+) -> impl Iterator<Item = Exec> + 'a {
+    METADATA
+        .packages
+        .iter()
+        .filter(move |p| package.matches(&p.name))
+        .flat_map(move |p| {
+            p.targets
+                .iter()
+                .filter(move |t| {
+                    name.matches(&t.name)
+                        && t.kind
+                            .get(0)
+                            .map_or(false, |kind| kinds.contains(&kind.as_str()))
+                })
+                .filter_map(|t| match t.kind.get(0)?.as_str() {
+                    "bin" => Some(Exec::Bin {
+                        pkg: &p.name,
+                        name: &t.name,
+                    }),
+                    "example" => Some(Exec::Example {
+                        pkg: &p.name,
+                        name: &t.name,
+                    }),
+                    "test" => Some(Exec::Test {
+                        pkg: &p.name,
+                        name: &t.name,
+                    }),
+                    "bench" => Some(Exec::Bench {
+                        pkg: &p.name,
+                        name: &t.name,
+                    }),
+                    "lib" => Some(Exec::Lib {
+                        pkg: &p.name,
+                        name: &t.name,
+                    }),
+                    "proc-macro" => Some(Exec::ProcMacro {
+                        pkg: &p.name,
+                        name: &t.name,
+                    }),
+                    _ => None,
+                })
+        })
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum Exec {
+    Bin {
+        pkg: &'static str,
+        name: &'static str,
+    },
+    Example {
+        pkg: &'static str,
+        name: &'static str,
+    },
+    Test {
+        pkg: &'static str,
+        name: &'static str,
+    },
+    Bench {
+        pkg: &'static str,
+        name: &'static str,
+    },
+    Lib {
+        pkg: &'static str,
+        name: &'static str,
+    },
+    ProcMacro {
+        pkg: &'static str,
+        name: &'static str,
+    },
+}
+
+impl Exec {
+    pub fn matches(&self, package: Option<&str>, target: &Target) -> bool {
+        let (&pkg, &name, kind) = match self {
+            Exec::Bin { pkg, name } => (pkg, name, "bin"),
+            Exec::Example { pkg, name } => (pkg, name, "example"),
+            Exec::Test { pkg, name } => (pkg, name, "test"),
+            Exec::Bench { pkg, name } => (pkg, name, "bench"),
+            Exec::Lib { pkg, name } => (pkg, name, "lib"),
+            Exec::ProcMacro { pkg, name } => (pkg, name, "lib"),
+        };
+        name == target.name
+            && package.map_or(true, |p| p == pkg)
+            && target.kind.get(0).map(String::as_str) == Some(kind)
+    }
+
+    pub fn name(self) -> &'static str {
+        match self {
+            Exec::Bin { name, .. }
+            | Exec::Example { name, .. }
+            | Exec::Test { name, .. }
+            | Exec::Bench { name, .. }
+            | Exec::ProcMacro { name, .. }
+            | Exec::Lib { name, .. } => name,
+        }
+    }
+
+    pub fn pkg(self) -> &'static str {
+        match self {
+            Exec::Bin { pkg, .. }
+            | Exec::Lib { pkg, .. }
+            | Exec::Example { pkg, .. }
+            | Exec::Test { pkg, .. }
+            | Exec::ProcMacro { pkg, .. }
+            | Exec::Bench { pkg, .. } => pkg,
+        }
+    }
+
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        match self {
+            Exec::Bin { pkg, name } => cmd.args(["--package", pkg, "--bin", name]),
+            Exec::Example { pkg, name } => cmd.args(["--package", pkg, "--example", name]),
+            Exec::Test { pkg, name } => cmd.args(["--package", pkg, "--test", name]),
+            Exec::Bench { pkg, name } => cmd.args(["--package", pkg, "--bench", name]),
+            Exec::ProcMacro { pkg, .. } => cmd.args(["--package", pkg, "--lib"]),
+            Exec::Lib { pkg, .. } => cmd.args(["--package", pkg, "--lib"]),
+        };
+    }
+}
diff --git a/bpaf_cauwugo/src/opts.rs b/bpaf_cauwugo/src/opts.rs
index f64a077..2220e8b 100644
--- a/bpaf_cauwugo/src/opts.rs
+++ b/bpaf_cauwugo/src/opts.rs
@@ -1,298 +1,146 @@
-use std::{ffi::OsString, sync::Mutex};
+use std::cell::RefCell;
+use std::{collections::BTreeSet, rc::Rc};
 
-use bpaf::{positional, Bpaf, CompleteDecor, Parser};
-use cargo_metadata::{CargoOpt, Metadata, MetadataCommand};
-use once_cell::sync::Lazy;
+use bpaf::*;
 
-// read cargo metadata
-#[derive(Copy, Debug, Clone, PartialEq, Eq)]
-pub enum Kind {
-    Bin,
-    Example,
-}
-
-#[derive(Debug, Clone)]
-pub struct Exec<'a> {
-    pub package: &'a str,
-    pub name: &'a str,
-    pub kind: Kind,
-}
-
-pub static EXECS: Lazy<Vec<Exec<'static>>> = Lazy::new(|| {
-    let mut execs = Vec::new();
-    for package in &METADATA.packages {
-        for target in &package.targets {
-            let kind;
-            if target.kind[0] == "bin" {
-                kind = Kind::Bin;
-            } else if target.kind[0] == "example" {
-                kind = Kind::Example;
-            } else {
-                continue;
-            }
-            execs.push(Exec {
-                package: &package.name,
-                name: &target.name,
-                kind,
-            });
-        }
-    }
-    execs
-});
-
-pub static METADATA: Lazy<Metadata> = Lazy::new(|| {
-    MetadataCommand::new()
-        .manifest_path("./Cargo.toml")
-        .features(CargoOpt::AllFeatures)
-        .no_deps()
-        .exec()
-        .unwrap()
-});
-
-#[derive(Debug, Default, Bpaf, Clone)]
-#[bpaf(complete_style(CompleteDecor::VisibleGroup("== Cargo options")))]
-pub struct CargoParams {
-    #[bpaf(
-        short,
-        long,
-        argument("SPEC"),
-        complete(complete_package),
-        map(remember_package),
-        optional
-    )]
-    package: Option<String>,
-
-    #[bpaf(external, map(remember_binary), optional)]
-    binary: Option<Binary>,
-    #[bpaf(switch, map(remember_release))]
-    release: bool,
-    //    quiet: bool,
-    //    verbose: usize,
-}
-
-// this exists so cauwugo can refer to that while trying to complete arguments
-// for the binaries, this is not needed for usual argument parsing
-pub static CARGO_PARAMS: Lazy<Mutex<CargoParams>> =
-    Lazy::new(|| Mutex::new(CargoParams::default()));
+use crate::add::{parse_add, Add};
+use crate::build::{parse_build, Build};
+use crate::check::{parse_check, Check};
+use crate::clean::{parse_clean, Clean};
+use crate::metadata::{matching_targets, Exec, MatchKind};
+use crate::run::{parse_run, Run};
+use crate::test::{parse_test, Test};
+use crate::unique_match;
 
 #[derive(Debug, Clone, Bpaf)]
-pub enum Binary {
-    Bin(#[bpaf(long("bin"), argument("NAME"), complete(complete_binary))] String),
-    Example(#[bpaf(long("example"), argument("NAME"), complete(complete_example))] String),
-}
-
-fn complete_binary(input: &String) -> Vec<(String, Option<String>)> {
-    let mut res = Vec::new();
-    for exe in EXECS.iter() {
-        if exe.kind == Kind::Bin && exe.name.starts_with(input) {
-            res.push((exe.name.to_string(), Some(format!("({})", exe.package))));
-        }
-    }
-    res
+pub enum Cauwugo {
+    Add(#[bpaf(external(parse_add))] Add),
+    Build(#[bpaf(external(parse_build))] Build),
+    Check(#[bpaf(external(parse_check))] Check),
+    Run(#[bpaf(external(parse_run))] Run),
+    Clean(#[bpaf(external(parse_clean))] Clean),
+    Test(#[bpaf(external(parse_test))] Test),
+    // bench ?
 }
 
-fn complete_example(input: &String) -> Vec<(String, Option<String>)> {
-    let mut res = Vec::new();
-    let params = &CARGO_PARAMS.lock().unwrap();
-    for exe in EXECS.iter() {
-        if params.package.as_ref().map_or(false, |p| exe.package != p) {
-            continue;
-        }
-
-        if exe.kind == Kind::Example && exe.name.starts_with(input) {
-            res.push((exe.name.to_string(), Some(format!("({})", exe.package))));
-        }
-    }
-    res
-}
-
-fn complete_package(input: &String) -> Vec<(String, Option<String>)> {
-    let mut res = Vec::new();
-    for pkg in &METADATA.packages {
-        let name = &pkg.name;
-        if name.starts_with(input) {
-            res.push((name.clone(), None))
-        }
-    }
-    res
-}
-
-pub fn cargo_command(
-    cargo_command: &'static str,
-    opts: &CargoParams,
-    exec: Option<&Exec<'static>>,
-) -> std::process::Command {
-    let mut cmd = std::process::Command::new("cargo");
-    cmd.arg(cargo_command);
-
-    if let Some(exec) = &exec {
-        cmd.arg("--package").arg(exec.package);
-        match exec.kind {
-            Kind::Bin => cmd.arg("--bin").arg(exec.name),
-            Kind::Example => cmd.arg("--example").arg(exec.name),
-        };
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(options, version)]
+/// |      _~^~^~_
+/// |  \) / >   < \ (/   run cargo commands with the power of dynamic completion
+/// |    '_   ω   _'
+/// |   /  '-----' /
+pub struct CauwugoOpts {
+    /// Cauwugo will print underlying cargo command before proceeding
+    pub bpaf_verbose: bool,
+
+    /// Cauwugo won't perform actions, only show the intended command line
+    pub bpaf_dry: bool,
+
+    #[bpaf(external)]
+    pub cauwugo: Cauwugo,
+}
+
+/// Fill in completion suggestions for all the previous inputs and all the available items
+///
+/// `in_vec` considers last item of inputs as one being currently completed and matches
+/// it as prefix, all other items - as already completed, so it uses an exact match
+pub fn suggest_available<'a, I, S>(inputs: &[S], avail: I) -> Vec<(String, Option<String>)>
+where
+    I: Iterator<Item = &'a str>,
+    S: AsRef<str>,
+{
+    if inputs.is_empty() {
+        avail.map(|a| (a.to_owned(), None)).collect::<Vec<_>>()
     } else {
-        if let Some(package) = &opts.package {
-            cmd.arg("--package").arg(package);
-        }
-        if let Some(binary) = &opts.binary {
-            match binary {
-                Binary::Bin(name) => cmd.arg("--bin").arg(name),
-                Binary::Example(name) => cmd.arg("--example").arg(name),
-            };
-        }
+        let active = inputs.last();
+        let complete = inputs[..inputs.len() - 1]
+            .iter()
+            .map(AsRef::as_ref)
+            .collect::<BTreeSet<_>>();
+        avail
+            .filter_map(|av| {
+                if complete.contains(av) {
+                    None
+                } else if active.map_or(true, |ac| av.starts_with(ac.as_ref())) {
+                    Some((av.to_owned(), None))
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>()
     }
-    cmd
 }
 
-fn complete_binary_args(input: &Vec<OsString>) -> Vec<(String, Option<String>)> {
-    if input.is_empty() {
-        return vec![(
-            "--".to_owned(),
-            Some("proceed to inner app arguments".to_owned()),
-        )];
-    }
-    let mut cmd = cargo_command("run", &CARGO_PARAMS.lock().unwrap(), None);
-
-    let rev = std::env::args_os().nth(1).unwrap();
-
-    let output = cmd
-        .arg("--quiet")
-        .arg("--")
-        .arg(rev)
-        .args(input.as_slice())
-        .stdout(std::process::Stdio::piped())
-        .spawn()
-        .unwrap()
-        .wait_with_output()
-        .unwrap();
-
-    let mut res = Vec::new();
-
-    for line in std::str::from_utf8(&output.stdout).unwrap().lines() {
-        res.push(match line.split_once('\t') {
-            Some((arg, decor)) => (arg.to_owned(), Some(decor.to_owned())),
-            None => (line.to_owned(), None),
+pub fn complete_target_kind<S: AsRef<str>>(
+    input: &[S],
+    package: Option<&'static str>,
+    kinds: &'static [&'static str],
+) -> Vec<(String, Option<String>)> {
+    let targets =
+        matching_targets(MatchKind::exact(package), MatchKind::Any, kinds).map(Exec::name);
+    suggest_available(input, targets)
+}
+
+pub fn parse_runnable(package: Rc<RefCell<Option<&'static str>>>) -> impl Parser<Exec> {
+    let bin = parse_bin(package.clone());
+    let example = parse_example(package.clone());
+
+    let package0 = package.clone();
+    const RUNNABLE: &[&str] = &["bin", "example"];
+    let pos = positional::<String>("EXE")
+        .complete(move |i| complete_target_kind(&[i], *package.borrow(), RUNNABLE))
+        .parse::<_, _, String>(move |name| {
+            unique_match(
+                matching_targets(
+                    MatchKind::exact(*package0.borrow()),
+                    MatchKind::Exact(&name),
+                    RUNNABLE,
+                ),
+                &name,
+            )
+        })
+        .complete_style(CompleteDecor::VisibleGroup("Binaries and examples"));
+
+    construct!([pos, bin, example])
+}
+
+pub fn parse_testable(package: Rc<RefCell<Option<&'static str>>>) -> impl Parser<Exec> {
+    let bin = parse_bin(package.clone());
+    let example = parse_example(package.clone());
+    let test = parse_exec_target('t', "test", "use this test", &["test"], package.clone());
+    let bench = parse_exec_target('b', "bench", "use this benchmark", &["bench"], package);
+    construct!([bin, example, test, bench])
+}
+
+fn parse_bin(package: Rc<RefCell<Option<&'static str>>>) -> impl Parser<Exec> {
+    parse_exec_target('b', "bin", "use this binary", &["bin"], package)
+}
+
+fn parse_example(package: Rc<RefCell<Option<&'static str>>>) -> impl Parser<Exec> {
+    parse_exec_target('e', "example", "use this example", &["example"], package)
+}
+
+fn parse_exec_target(
+    short_name: char,
+    long_name: &'static str,
+    help: &'static str,
+    kinds: &'static [&'static str],
+    package: Rc<RefCell<Option<&'static str>>>,
+) -> impl Parser<Exec> {
+    let package0 = package.clone();
+    long(long_name)
+        .short(short_name)
+        .help(help)
+        .argument::<String>("NAME")
+        .complete(move |i| complete_target_kind(&[i], *package.borrow(), kinds))
+        .parse::<_, _, String>(move |name| {
+            unique_match(
+                matching_targets(
+                    MatchKind::exact(*package0.borrow()),
+                    MatchKind::Exact(&name),
+                    kinds,
+                ),
+                &name,
+            )
         })
-    }
-
-    res
-}
-
-fn remember_package(package: String) -> String {
-    CARGO_PARAMS.lock().unwrap().package = Some(package.clone());
-    package
-}
-
-fn remember_binary(binary: Binary) -> Binary {
-    CARGO_PARAMS.lock().unwrap().binary = Some(binary.clone());
-    binary
-}
-
-fn remember_release(release: bool) -> bool {
-    CARGO_PARAMS.lock().unwrap().release = release;
-    release
-}
-
-fn child_process_args() -> impl Parser<Vec<OsString>> {
-    positional::<OsString>("CHILD_ARG")
-        .strict()
-        .many()
-        .complete(complete_binary_args)
-}
-
-#[allow(clippy::ptr_arg)]
-fn complete_ws_binary(name: &String) -> Vec<(String, Option<String>)> {
-    let mut res = Vec::new();
-    let params = CARGO_PARAMS.lock().unwrap();
-    for exec in EXECS.iter() {
-        if !exec.name.starts_with(name) {
-            continue;
-        }
-        if let Some(pkg) = &params.package {
-            if exec.package != pkg {
-                continue;
-            }
-        }
-        if let Some(bin) = &params.binary {
-            if match bin {
-                Binary::Bin(name) => !(exec.kind == Kind::Bin && exec.name == name),
-                Binary::Example(name) => !(exec.kind == Kind::Example && exec.name == name),
-            } {
-                continue;
-            }
-        }
-        res.push((exec.name.to_owned(), None));
-    }
-    res
-}
-
-fn parse_ws_binary(name: String) -> Result<Exec<'static>, String> {
-    let exec = EXECS
-        .iter()
-        .find(|e| e.name == name)
-        .cloned()
-        .ok_or_else(|| format!("{} is not a valid exec name", name))?;
-    let params = &mut CARGO_PARAMS.lock().unwrap();
-    params.package = Some(exec.package.to_owned());
-    params.binary = Some(match exec.kind {
-        Kind::Bin => Binary::Bin(exec.name.to_owned()),
-        Kind::Example => Binary::Example(exec.name.to_owned()),
-    });
-
-    Ok(exec)
-}
-
-fn pick_binary() -> impl Parser<Exec<'static>> {
-    positional::<String>("BIN")
-        .help("binary or executable name available in a workspace")
-        .complete(complete_ws_binary)
-        .complete_style(CompleteDecor::VisibleGroup("== Workspace binaries"))
-        .parse(parse_ws_binary)
-}
-
-#[derive(Debug, Clone, Bpaf)]
-#[bpaf(options)]
-pub enum Options {
-    #[bpaf(command)]
-    Run {
-        #[bpaf(external)]
-        cargo_params: CargoParams,
-
-        #[bpaf(external(pick_binary), optional)]
-        exec: Option<Exec<'static>>,
-
-        #[bpaf(external(child_process_args))]
-        args: Vec<OsString>,
-    },
 }
-
-/*
-    -q, --quiet                     Do not print cargo log messages
-        --bin <NAME>                Name of the bin target to run
-        --example <NAME>            Name of the example target to run
-    -p, --package [<SPEC>...]       Package with the target to run
-    -v, --verbose                   Use verbose output (-vv very verbose/build.rs output)
-    -j, --jobs <N>                  Number of parallel jobs, defaults to # of CPUs
-        --color <WHEN>              Coloring: auto, always, never
-        --keep-going                Do not abort the build as soon as there is an error (unstable)
-        --frozen                    Require Cargo.lock and cache are up to date
-    -r, --release                   Build artifacts in release mode, with optimizations
-        --locked                    Require Cargo.lock is up to date
-        --profile <PROFILE-NAME>    Build artifacts with the specified profile
-    -F, --features <FEATURES>       Space or comma separated list of features to activate
-        --offline                   Run without accessing the network
-        --all-features              Activate all available features
-        --config <KEY=VALUE>        Override a configuration value (unstable)
-        --no-default-features       Do not activate the `default` feature
-    -Z <FLAG>                       Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for
-                                    details
-        --target <TRIPLE>           Build for the target triple
-        --target-dir <DIRECTORY>    Directory for all generated artifacts
-        --manifest-path <PATH>      Path to Cargo.toml
-        --message-format <FMT>      Error format
-        --unit-graph                Output build graph in JSON (unstable)
-        --ignore-rust-version       Ignore `rust-version` specification in packages
-        --timings[=<FMTS>...]       Timing output formats (unstable) (comma separated): html, json
-    -h, --help                      Print help information
-*/
diff --git a/bpaf_cauwugo/src/run.rs b/bpaf_cauwugo/src/run.rs
new file mode 100644
index 0000000..3e95703
--- /dev/null
+++ b/bpaf_cauwugo/src/run.rs
@@ -0,0 +1,103 @@
+use crate::{
+    metadata::{bpaf_passthough_for, Exec},
+    opts::parse_runnable,
+    remember_opt, remember_req,
+    shared::{cargo_opts, parse_package, CargoOpts},
+};
+use bpaf::*;
+use std::{cell::RefCell, ffi::OsString, process::Command, rc::Rc};
+
+#[derive(Debug, Clone)]
+pub struct Run {
+    pub cargo_opts: CargoOpts,
+    pub package: Option<&'static str>,
+    pub runnable: Exec,
+    pub args: Vec<OsString>,
+}
+
+impl Run {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        cmd.arg("run");
+        self.cargo_opts.pass_to_cmd(cmd);
+        if let Some(p) = self.package {
+            cmd.args(["--package", p]);
+        }
+        self.runnable.pass_to_cmd(cmd);
+        cmd.arg("--");
+        cmd.args(&self.args);
+    }
+}
+
+/// This one is a hack. While it is easy to get the lines from child process - it's harder
+/// to convert them to the expected output. Luckily all we want is to dump them in the same format
+/// as we got them and nothing else. So not capturing the output + exit is the obvious choice
+fn complete_binary_args(input: &[OsString], exec: Option<Exec>) -> Vec<(String, Option<String>)> {
+    if input.is_empty() {
+        return vec![(
+            "--".to_owned(),
+            Some("proceed to inner app arguments".to_owned()),
+        )];
+    }
+
+    if !bpaf_passthough_for(exec.map_or("", Exec::pkg)) {
+        return vec![(
+            "<ARG>".to_owned(),
+            Some("argument completion requires bpaf passthough enabled".to_owned()),
+        )];
+    }
+
+    let mut cmd = Command::new("cargo");
+    cmd.arg("run");
+    if let Some(exec) = exec {
+        exec.pass_to_cmd(&mut cmd);
+    }
+
+    let revision = std::env::args_os().nth(1).unwrap();
+
+    cmd.arg("--quiet").arg("--").arg(revision).args(input);
+
+    let output = cmd
+        .stderr(std::process::Stdio::piped())
+        .spawn()
+        .unwrap()
+        .wait_with_output();
+
+    match output {
+        Ok(ok) => {
+            if ok.status.success() {
+                std::process::exit(0);
+            } else {
+                eprint!("{}", std::str::from_utf8(&ok.stderr).unwrap());
+                std::process::exit(ok.status.code().unwrap_or(1));
+            }
+        }
+        Err(err) => {
+            panic!("{}", err);
+        }
+    }
+}
+
+pub fn parse_run() -> impl Parser<Run> {
+    let cur_pkg = Rc::new(RefCell::new(None));
+    let cur_exec = Rc::new(RefCell::new(None));
+
+    let package = remember_opt(parse_package("Package with the target to run"), &cur_pkg);
+    let runnable = remember_req(parse_runnable(cur_pkg), &cur_exec);
+
+    let args = positional::<OsString>("args")
+        .help("Cauwugo will pass arguments after -- to the child process")
+        .strict()
+        .many()
+        .complete(move |input| complete_binary_args(input, *cur_exec.borrow()));
+
+    construct!(Run {
+        cargo_opts(),
+        package,
+        runnable,
+        args,
+    })
+    .to_options()
+    .descr("Run a binary or example of the local package")
+    .command("run")
+    .short('r')
+}
diff --git a/bpaf_cauwugo/src/shared.rs b/bpaf_cauwugo/src/shared.rs
new file mode 100644
index 0000000..00c2ac4
--- /dev/null
+++ b/bpaf_cauwugo/src/shared.rs
@@ -0,0 +1,117 @@
+use bpaf::*;
+use std::{cell::RefCell, process::Command, rc::Rc};
+
+use crate::{
+    metadata::{Exec, METADATA},
+    opts::{parse_testable, suggest_available},
+};
+
+/// A set of options somewhat shared between different cargo commands
+///
+/// Some are redundant :)
+#[derive(Debug, Clone, Bpaf)]
+pub struct CargoOpts {
+    /// Use release mode with optimizations
+    #[bpaf(short, long)]
+    pub release: bool,
+
+    /// Don't print cargo log messages
+    #[bpaf(short, long)]
+    pub quiet: bool,
+
+    /// Require Cargo.lock and cache are up to date
+    pub frozen: bool,
+
+    /// Require Cargo.lock is up to date
+    pub locked: bool,
+
+    /// Run without accessing the network
+    pub offline: bool,
+
+    /// Number of parallel jobs, defaults to # of CPUs
+    pub jobs: Option<usize>,
+
+    #[bpaf(argument("TRIPLE"), complete(complete_available_target), optional)]
+    /// Build for the target triple
+    pub target: Option<String>,
+}
+
+impl CargoOpts {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        pass_flag!(cmd, self.release, "--release");
+        pass_flag!(cmd, self.quiet, "--quiet");
+        pass_flag!(cmd, self.frozen, "--frozen");
+        pass_flag!(cmd, self.locked, "--locked");
+        pass_flag!(cmd, self.offline, "--offline");
+        pass_arg!(cmd, self.jobs.map(|j| j.to_string()), "--jobs");
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct PackageAndTestables {
+    pub package: Option<&'static str>,
+    pub testables: Vec<Exec>,
+}
+
+impl PackageAndTestables {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        for t in &self.testables {
+            t.pass_to_cmd(cmd)
+        }
+        pass_arg!(cmd, self.package, "--package");
+    }
+}
+
+pub fn parse_package(help: &'static str) -> impl Parser<Option<&'static str>> {
+    short('p')
+        .long("package")
+        .help(help)
+        .argument::<String>("NAME")
+        .complete(|i| suggest_available(&[i], METADATA.packages.iter().map(|p| p.name.as_str())))
+        .parse(|i| {
+            METADATA
+                .packages
+                .iter()
+                .find_map(|p| {
+                    if p.name == i {
+                        Some(p.name.as_str())
+                    } else {
+                        None
+                    }
+                })
+                .ok_or_else(|| format!("{} is not a known package", i))
+        })
+        .optional()
+}
+
+pub fn package_and_testables() -> impl Parser<PackageAndTestables> {
+    let cur_pkg = Rc::new(RefCell::new(None));
+    let testables = parse_testable(cur_pkg.clone()).many();
+    let package = parse_package("Package to check").map(move |p| {
+        *cur_pkg.borrow_mut() = p;
+        p
+    });
+    construct!(PackageAndTestables { package, testables })
+}
+
+#[allow(clippy::ptr_arg)]
+fn complete_available_target(input: &String) -> Vec<(String, Option<String>)> {
+    let mut cmd = Command::new("rustup");
+    cmd.args(["target", "list", "--installed"]);
+    let output = cmd.output().expect("Couldn't run rustup");
+    if output.status.success() {
+        String::from_utf8_lossy(&output.stdout)
+            .lines()
+            .filter_map(|l| {
+                if l.starts_with(input) {
+                    Some((l.to_owned(), None))
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>()
+    } else {
+        eprint!("{}", std::str::from_utf8(&output.stderr).unwrap());
+        panic!("Couldn't get list of installed targets from rustup");
+    }
+}
diff --git a/src/args.rs b/src/args.rs
index 84abc97..c365315 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -195,8 +195,27 @@ mod inner {
 
         #[cfg(feature = "autocomplete")]
         /// enable completions with custom output revision style
+        ///
+        ///
+        /// # Panics
+        /// Contains some assertions which shouldn't trigger in normal operation
         #[must_use]
         pub fn set_comp(mut self, rev: usize) -> Self {
+            // last item on a command line is "--" it might be both double dash indicator
+            // "the rest are strictly positionals" but it can also be part of the long name
+            // restore it so completion logic is more internally consistent
+            if !self.removed.is_empty() {
+                let o = self.removed.len() - 1;
+                if self.removed[o] {
+                    self.removed[o] = false;
+                    self.remaining += 1;
+                    if let Arg::PosWord(w) = &self.items[o] {
+                        assert_eq!(w, "--");
+                    } else {
+                        panic!("Last item is strange {:?}, this is a bug", self);
+                    }
+                }
+            }
             self.comp = Some(crate::complete_gen::Complete::new(rev));
             self
         }


Test patch:
diff --git a/bpaf_cauwugo/src/test.rs b/bpaf_cauwugo/src/test.rs
new file mode 100644
index 0000000..3a2722a
--- /dev/null
+++ b/bpaf_cauwugo/src/test.rs
@@ -0,0 +1,143 @@
+use std::{cell::RefCell, process::Command, rc::Rc};
+
+use bpaf::*;
+
+use crate::{
+    metadata::{matching_targets, Exec, MatchKind},
+    opts::complete_target_kind,
+    shared::{cargo_opts, package_and_testables, CargoOpts, PackageAndTestables},
+};
+
+#[derive(Debug, Clone)]
+pub enum Test {
+    All(Tests),
+    Specific(Specific),
+}
+
+impl Test {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        cmd.arg("test");
+        match self {
+            Test::All(tests) => tests.pass_to_cmd(cmd),
+            Test::Specific(spec) => {
+                spec.test.pass_to_cmd(cmd);
+                if let Some(name) = &spec.name {
+                    cmd.args(["--", "--exact", name]);
+                }
+            }
+        }
+    }
+}
+
+#[derive(Debug, Clone, Bpaf)]
+#[bpaf(generate(parse_test_general))]
+pub struct Tests {
+    #[bpaf(external)]
+    pub cargo_opts: CargoOpts,
+
+    /// Check the library
+    pub lib: bool,
+
+    /// Check all the binaries
+    pub bins: bool,
+
+    /// Check all examples
+    pub examples: bool,
+
+    /// Check all tests
+    pub tests: bool,
+
+    /// Check all benchmarks
+    pub benches: bool,
+
+    #[bpaf(external)]
+    pub package_and_testables: PackageAndTestables,
+}
+
+impl Tests {
+    pub fn pass_to_cmd(&self, cmd: &mut Command) {
+        self.package_and_testables.pass_to_cmd(cmd);
+        self.cargo_opts.pass_to_cmd(cmd);
+        pass_flag!(cmd, self.lib, "--lib");
+        pass_flag!(cmd, self.bins, "--bins");
+        pass_flag!(cmd, self.examples, "--examples");
+        pass_flag!(cmd, self.benches, "--benches");
+        pass_flag!(cmd, self.tests, "--tests");
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct Specific {
+    pub test: Exec,
+    pub name: Option<String>,
+}
+
+const TESTABLE: &[&str] = &["test", "lib", "proc-macro"];
+
+fn complete_subtest_name(input: &str, current_test: Option<Exec>) -> Vec<(String, Option<String>)> {
+    let current_test = match current_test {
+        Some(t) => t,
+        None => return Vec::new(),
+    };
+
+    let mut cmd = std::process::Command::new("cargo");
+    cmd.args(["test", "--quiet"]);
+    current_test.pass_to_cmd(&mut cmd);
+    cmd.args(["--", "--list"]);
+
+    let output = cmd.output().unwrap();
+    if let Some(0) = output.status.code() {
+        std::str::from_utf8(&output.stdout)
+            .unwrap()
+            .lines()
+            .filter_map(|l| {
+                let test_name = l.strip_suffix(": test")?;
+                if test_name.starts_with(input) {
+                    Some((test_name.to_owned(), None))
+                } else {
+                    None
+                }
+            })
+            .collect::<Vec<_>>()
+    } else {
+        eprintln!("{}", std::str::from_utf8(&output.stderr).unwrap());
+        std::process::exit(1)
+    }
+}
+
+pub fn parse_specific() -> impl Parser<Test> {
+    let cur_exec = Rc::new(RefCell::new(None));
+    let cur_exec2 = cur_exec.clone();
+    let test = positional::<String>("TEST")
+        .help("Test file name")
+        .complete(move |i| complete_target_kind(&[i], None, TESTABLE))
+        .parse::<_, _, String>(move |name| {
+            let mut iter = matching_targets(MatchKind::Any, MatchKind::Exact(&name), TESTABLE);
+            match (iter.next(), iter.next()) {
+                (None, _) => Err(format!("{} is not a known test name", name)),
+                (Some(_), Some(_)) => Err(format!("{} is not a unique test name", name)),
+                (Some(exec), None) => {
+                    *cur_exec.borrow_mut() = Some(exec);
+                    Ok(exec)
+                }
+            }
+        })
+        .complete_style(CompleteDecor::VisibleGroup("Bins, tests, examples"));
+
+    let name = positional::<String>("NAME")
+        .help("Test name in a file")
+        .complete(move |i| complete_subtest_name(i, *cur_exec2.borrow()))
+        .complete_style(CompleteDecor::VisibleGroup("Available test names"))
+        .optional();
+
+    construct!(Specific { test, name }).map(Test::Specific)
+}
+
+pub fn parse_test() -> impl Parser<Test> {
+    let general = parse_test_general().map(Test::All);
+    construct!([parse_specific(), general])
+        .to_options()
+        .descr("Execute unit and integration tests")
+        .command("test")
+        .short('t')
+}
diff --git a/tests/completion.rs b/tests/completion.rs
index 28f7cb0..d4555c2 100644
--- a/tests/completion.rs
+++ b/tests/completion.rs
@@ -474,20 +474,26 @@ fn just_positional() {
     assert_eq!(r, "xxx\n");
 }
 
+fn test_completer(input: &String) -> Vec<(&'static str, Option<&'static str>)> {
+    let mut vec = test_completer_descr(input);
+    vec.iter_mut().for_each(|i| i.1 = None);
+    vec
+}
+
+fn test_completer_descr(input: &String) -> Vec<(&'static str, Option<&'static str>)> {
+    let items = ["alpha", "beta", "banana", "cat", "durian"];
+    items
+        .iter()
+        .filter(|item| item.starts_with(input))
+        .map(|item| (*item, Some(*item)))
+        .collect::<Vec<_>>()
+}
+
 #[test]
 fn dynamic_complete_test_1() {
-    fn completer(input: &String) -> Vec<(&'static str, Option<&'static str>)> {
-        let items = ["alpha", "beta", "banana", "cat", "durian"];
-        items
-            .iter()
-            .filter(|item| item.starts_with(input))
-            .map(|item| (*item, None))
-            .collect::<Vec<_>>()
-    }
-
     let parser = short('a')
         .argument::<String>("ARG")
-        .complete(completer)
+        .complete(test_completer)
         .to_options();
 
     let r = parser
@@ -535,22 +541,13 @@ fn dynamic_complete_test_2() {
 
 #[test]
 fn dynamic_complete_test_3() {
-    fn complete_calculator(input: &String) -> Vec<(&'static str, Option<&'static str>)> {
-        let items = ["alpha", "beta", "banana", "cat", "durian"];
-        items
-            .iter()
-            .filter(|item| item.starts_with(input))
-            .map(|item| (*item, None))
-            .collect::<Vec<_>>()
-    }
-
     let a = short('a').long("avocado").help("Use avocado").switch();
     let b = short('b').long("banana").help("Use banana").switch();
     let bb = long("bananananana").help("I'm Batman").switch();
     let c = long("calculator")
         .help("calculator expression")
         .argument::<String>("EXPR")
-        .complete(complete_calculator);
+        .complete(test_completer);
     let parser = construct!(a, b, bb, c).to_options();
 
     let r = parser
@@ -562,18 +559,9 @@ fn dynamic_complete_test_3() {
 
 #[test]
 fn dynamic_complete_test_4() {
-    fn complete_calculator(input: &String) -> Vec<(&'static str, Option<&'static str>)> {
-        let names = ["Yuri", "Lupusregina", "Solution", "Shizu", "Entoma"];
-        names
-            .iter()
-            .filter(|item| item.starts_with(input))
-            .map(|item| (*item, Some(*item)))
-            .collect::<Vec<_>>()
-    }
-
     let parser = long("name")
         .argument::<String>("NAME")
-        .complete(complete_calculator)
+        .complete(test_completer_descr)
         .to_options();
 
     let r = parser
@@ -582,14 +570,14 @@ fn dynamic_complete_test_4() {
         .unwrap_stdout();
     assert_eq!(
         r,
-        "Yuri\tYuri\nLupusregina\tLupusregina\nSolution\tSolution\nShizu\tShizu\nEntoma\tEntoma\n"
+        "alpha\talpha\nbeta\tbeta\nbanana\tbanana\ncat\tcat\ndurian\tdurian\n"
     );
 
     let r = parser
-        .run_inner(Args::from(&["--name", "L"]).set_comp(1))
+        .run_inner(Args::from(&["--name", "a"]).set_comp(1))
         .unwrap_err()
         .unwrap_stdout();
-    assert_eq!(r, "Lupusregina\n");
+    assert_eq!(r, "alpha\n");
 }
 
 #[test]
@@ -620,22 +608,13 @@ fn static_with_fallback_and_hide() {
 
 #[test]
 fn csample_mystery() {
-    fn complete_calculator(input: &String) -> Vec<(&'static str, Option<&'static str>)> {
-        let items = ["alpha", "beta", "banana", "cat", "durian"];
-        items
-            .iter()
-            .filter(|item| item.starts_with(input))
-            .map(|item| (*item, None))
-            .collect::<Vec<_>>()
-    }
-
     let a = short('a').long("avocado").help("Use avocado").switch();
     let b = short('b').long("banana").help("Use banana").switch();
     let bb = long("bananananana").help("I'm Batman").switch();
     let c = long("calculator")
         .help("calculator expression")
         .argument::<String>("EXPR")
-        .complete(complete_calculator);
+        .complete(test_completer);
     let parser = construct!(a, b, bb, c)
         .to_options()
         .descr("Dynamic autocomplete example")
@@ -853,7 +832,7 @@ fn test_zsh_comp<T: std::fmt::Debug>(
         assert_eq!(&actual_words, expected, "on line {}", ix)
     }
 
-    assert_eq!(actual_len, expected_len);
+    assert_eq!(actual_len, expected_len, "<- actual, expected lengths ->");
 }
 
 #[test]
@@ -1042,3 +1021,17 @@ fn zsh_complete_info() {
         ],
     );
 }
+
+#[test]
+fn double_dash_as_positional() {
+    let parser = positional::<String>("P")
+        .help("Help")
+        .complete(test_completer)
+        .to_options();
+
+    test_zsh_comp(&parser, &["-"], &[]);
+    println!("\n\n");
+    test_zsh_comp(&parser, &["--"], &[]);
+    test_zsh_comp(&parser, &["a"], &[["alpha", "alpha", "", ""]]);
+    test_zsh_comp(&parser, &["x"], &[]);
+}