Repository: clap-rs/clap
Pull request #5621 (2024-08-02 16:51:05)

Support dynamic value of argument completion
<!--
Thanks for helping out!

Please link the appropriate issue from your PR.

If you don't have an issue, we'd recommend starting with one first so the PR can focus on the
implementation (unless its an obvious bug or documentation fix that will have
little conversation).
-->
Related issue https://github.com/clap-rs/clap/issues/1232

### Work in this PR
- Add `CustomCompleter` trait and `ArgValueCompleter` struct to provide users a way to add custom value of argument completion.
- Add test case to show the feat.

### What may be left
> Open question: does the completion function accept just the arg and current value or does it have access to `ArgMatches` for completing based on other data like in https://github.com/clap-rs/clap/issues/1910
- [ ]  Is there any way to access `ArgMatches` before the builder of the `Command`. We add custom value hint in builder now. If we really need this feat, we may need to do more things.


shannmu: Thanks for your review! Do you think it is necessary to prevent the spread of user code errors? I think it may be necessary, because the completion code is executed implicitly, and if the spread of errors is not prevented, it may cause confusion to users. Also, I think my current implementation is not good, maybe we can use other rust tools or crates to do this.
epage: > I think it may be necessary, because the completion code is executed implicitly, and if the spread of errors is not prevented, it may cause confusion to user

If the user wants panic prevention, they can provide it.  I don't think we need to be doing it for them

>  Also, I think my current implementation is not good, maybe we can use other rust tools or crates to do this.

Which parts are you concerned about?  Do you have something in mind?
shannmu: > Which parts are you concerned about? Do you have something in mind?

I mean the panic prevention part. I want to let the user code execute in a sandbox so as not to affect other possible completions.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index dfca8a36..a04c3626 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -101,9 +101,9 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.8"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
 
 [[package]]
 name = "anstyle-parse"
@@ -413,7 +413,6 @@ name = "clap"
 version = "4.5.13"
 dependencies = [
  "automod",
- "clap-cargo",
  "clap_builder 4.5.13",
  "clap_derive",
  "humantime",
@@ -424,15 +423,6 @@ dependencies = [
  "trycmd",
 ]
 
-[[package]]
-name = "clap-cargo"
-version = "0.14.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23b2ea69cefa96b848b73ad516ad1d59a195cdf9263087d977f648a818c8b43e"
-dependencies = [
- "anstyle",
-]
-
 [[package]]
 name = "clap_bench"
 version = "0.0.0"
@@ -1442,7 +1432,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
 dependencies = [
  "cfg-if",
- "windows-targets 0.52.6",
+ "windows-targets 0.48.5",
 ]
 
 [[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 3d69c8a2..d8706089 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -187,7 +187,6 @@ humantime = "2.1.0"
 snapbox = "0.6.16"
 shlex = "1.3.0"
 automod = "1.0.14"
-clap-cargo = { version = "0.14.1", default-features = false }
 
 [[example]]
 name = "demo"
@@ -195,11 +194,11 @@ required-features = ["derive"]
 
 [[example]]
 name = "cargo-example"
-required-features = ["cargo", "color"]
+required-features = ["cargo"]
 
 [[example]]
 name = "cargo-example-derive"
-required-features = ["derive", "color"]
+required-features = ["derive"]
 
 [[example]]
 name = "escaped-positional"
diff --git a/clap_builder/Cargo.toml b/clap_builder/Cargo.toml
index afb3f05a..827c31bc 100644
--- a/clap_builder/Cargo.toml
+++ b/clap_builder/Cargo.toml
@@ -63,7 +63,7 @@ clap_lex = { path = "../clap_lex", version = "0.7.0" }
 unicase = { version = "2.6.0", optional = true }
 strsim = { version = "0.11.0",  optional = true }
 anstream = { version = "0.6.7", optional = true }
-anstyle = "1.0.8"
+anstyle = "1.0.0"
 terminal_size = { version = "0.3.0", optional = true }
 backtrace = { version = "0.3.73", optional = true }
 unicode-width = { version = "0.1.9", optional = true }
diff --git a/clap_builder/src/builder/arg.rs b/clap_builder/src/builder/arg.rs
index 563d7ec3..0ba84ae3 100644
--- a/clap_builder/src/builder/arg.rs
+++ b/clap_builder/src/builder/arg.rs
@@ -1545,8 +1545,11 @@ impl Arg {
 
     /// Allow grouping of multiple values via a delimiter.
     ///
-    /// i.e. allow values (`val1,val2,val3`) to be parsed as three values (`val1`, `val2`,
-    /// and `val3`) instead of one value (`val1,val2,val3`).
+    /// i.e. should `--option=val1,val2,val3` be parsed as three values (`val1`, `val2`,
+    /// and `val3`) or as a single value (`val1,val2,val3`). Defaults to using `,` (comma) as the
+    /// value delimiter for all arguments that accept values (options and positional arguments)
+    ///
+    /// **NOTE:** implicitly sets [`Arg::action(ArgAction::Set)`]
     ///
     /// # Examples
     ///
diff --git a/clap_builder/src/builder/command.rs b/clap_builder/src/builder/command.rs
index 6480e28b..37d5802e 100644
--- a/clap_builder/src/builder/command.rs
+++ b/clap_builder/src/builder/command.rs
@@ -1199,13 +1199,13 @@ impl Command {
     /// ```no_run
     /// # use clap_builder as clap;
     /// # use clap::{Command, ColorChoice, builder::styling};
-    /// const STYLES: styling::Styles = styling::Styles::styled()
-    ///     .header(styling::AnsiColor::Green.on_default().bold())
-    ///     .usage(styling::AnsiColor::Green.on_default().bold())
-    ///     .literal(styling::AnsiColor::Blue.on_default().bold())
+    /// let styles = styling::Styles::styled()
+    ///     .header(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
+    ///     .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
+    ///     .literal(styling::AnsiColor::Blue.on_default() | styling::Effects::BOLD)
     ///     .placeholder(styling::AnsiColor::Cyan.on_default());
     /// Command::new("myprog")
-    ///     .styles(STYLES)
+    ///     .styles(styles)
     ///     .get_matches();
     /// ```
     #[cfg(feature = "color")]
diff --git a/clap_complete/Cargo.toml b/clap_complete/Cargo.toml
index 495ba99a..c24e0749 100644
--- a/clap_complete/Cargo.toml
+++ b/clap_complete/Cargo.toml
@@ -57,7 +57,7 @@ required-features = ["unstable-dynamic"]
 [features]
 default = []
 unstable-doc = ["unstable-dynamic"] # for docs.rs
-unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff"]
+unstable-dynamic = ["dep:clap_lex", "dep:shlex", "dep:unicode-xid", "clap/derive", "dep:is_executable", "dep:pathdiff", "clap/unstable-ext"]
 debug = ["clap/debug"]
 
 [lints]
diff --git a/clap_complete/src/dynamic/completer.rs b/clap_complete/src/dynamic/completer.rs
index 7a901539..9d3e62f4 100644
--- a/clap_complete/src/dynamic/completer.rs
+++ b/clap_complete/src/dynamic/completer.rs
@@ -1,7 +1,9 @@
-use core::num;
+use std::any::type_name;
 use std::ffi::OsStr;
 use std::ffi::OsString;
+use std::sync::Arc;
 
+use clap::builder::ArgExt;
 use clap::builder::StyledStr;
 use clap_lex::OsStrExt as _;
 
@@ -198,7 +200,7 @@ fn complete_arg(
                                             comp.get_content().to_string_lossy()
                                         ))
                                         .help(comp.get_help().cloned())
-                                        .visible(comp.is_visible())
+                                        .hide(comp.is_hide_set())
                                     }),
                             );
                         }
@@ -241,7 +243,6 @@ fn complete_arg(
                                 comp.get_content().to_string_lossy()
                             ))
                             .help(comp.get_help().cloned())
-                            .visible(true)
                         }),
                 );
             } else if let Some(short) = arg.to_short() {
@@ -271,7 +272,7 @@ fn complete_arg(
                                         comp.get_content().to_string_lossy()
                                     ))
                                     .help(comp.get_help().cloned())
-                                    .visible(comp.is_visible())
+                                    .hide(comp.is_hide_set())
                                 }),
                         );
                     } else {
@@ -283,7 +284,7 @@ fn complete_arg(
                                     comp.get_content().to_string_lossy()
                                 ))
                                 .help(comp.get_help().cloned())
-                                .visible(comp.is_visible())
+                                .hide(comp.is_hide_set())
                             },
                         ));
                     }
@@ -324,8 +325,8 @@ fn complete_arg(
             }
         }
     }
-    if completions.iter().any(|a| a.is_visible()) {
-        completions.retain(|a| a.is_visible());
+    if completions.iter().any(|a| !a.is_hide_set()) {
+        completions.retain(|a| !a.is_hide_set());
     }
 
     Ok(completions)
@@ -346,10 +347,16 @@ fn complete_arg_value(
                 name.starts_with(value).then(|| {
                     CompletionCandidate::new(OsString::from(name))
                         .help(p.get_help().cloned())
-                        .visible(!p.is_hide_set())
+                        .hide(p.is_hide_set())
                 })
             }));
         }
+    } else if let Some(completer) = arg.get::<ArgValueCompleter>() {
+        let value_os = match value {
+            Ok(value) => OsStr::new(value),
+            Err(value_os) => value_os,
+        };
+        values.extend(complete_custom_arg_value(value_os, completer));
     } else {
         let value_os = match value {
             Ok(value) => OsStr::new(value),
@@ -386,6 +393,7 @@ fn complete_arg_value(
                 values.extend(complete_path(value_os, current_dir, |_| true));
             }
         }
+
         values.sort();
     }
 
@@ -428,20 +436,14 @@ fn complete_path(
             let path = entry.path();
             let mut suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
             suggestion.push(""); // Ensure trailing `/`
-            completions.push(
-                CompletionCandidate::new(suggestion.as_os_str().to_owned())
-                    .help(None)
-                    .visible(true),
-            );
+            completions
+                .push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
         } else {
             let path = entry.path();
             if is_wanted(&path) {
                 let suggestion = pathdiff::diff_paths(&path, current_dir).unwrap_or(path);
-                completions.push(
-                    CompletionCandidate::new(suggestion.as_os_str().to_owned())
-                        .help(None)
-                        .visible(true),
-                );
+                completions
+                    .push(CompletionCandidate::new(suggestion.as_os_str().to_owned()).help(None));
             }
         }
     }
@@ -449,6 +451,21 @@ fn complete_path(
     completions
 }
 
+fn complete_custom_arg_value(
+    value: &OsStr,
+    completer: &ArgValueCompleter,
+) -> Vec<CompletionCandidate> {
+    debug!("complete_custom_arg_value: completer={completer:?}, value={value:?}");
+
+    let mut values = Vec::new();
+    let custom_arg_values = completer.0.completions();
+    values.extend(custom_arg_values);
+
+    values.retain(|comp| comp.get_content().starts_with(&value.to_string_lossy()));
+
+    values
+}
+
 fn complete_subcommand(value: &str, cmd: &clap::Command) -> Vec<CompletionCandidate> {
     debug!(
         "complete_subcommand: cmd={:?}, value={:?}",
@@ -476,7 +493,7 @@ fn longs_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
                 longs.into_iter().map(|s| {
                     CompletionCandidate::new(format!("--{}", s))
                         .help(a.get_help().cloned())
-                        .visible(!a.is_hide_set())
+                        .hide(a.is_hide_set())
                 })
             })
         })
@@ -494,7 +511,7 @@ fn hidden_longs_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
                 longs.into_iter().map(|s| {
                     CompletionCandidate::new(format!("--{}", s))
                         .help(a.get_help().cloned())
-                        .visible(false)
+                        .hide(true)
                 })
             })
         })
@@ -513,7 +530,7 @@ fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
                 shorts.into_iter().map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(a.get_help().cloned())
-                        .visible(!a.is_hide_set())
+                        .hide(a.is_hide_set())
                 })
             })
         })
@@ -546,12 +563,12 @@ fn subcommands(p: &clap::Command) -> Vec<CompletionCandidate> {
                 .map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(sc.get_about().cloned())
-                        .visible(!sc.is_hide_set())
+                        .hide(sc.is_hide_set())
                 })
                 .chain(sc.get_aliases().map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(sc.get_about().cloned())
-                        .visible(false)
+                        .hide(true)
                 }))
         })
         .collect()
@@ -667,8 +684,8 @@ pub struct CompletionCandidate {
     /// Help message with a completion candidate
     help: Option<StyledStr>,
 
-    /// Whether the completion candidate is visible
-    visible: bool,
+    /// Whether the completion candidate is hidden
+    hidden: bool,
 }
 
 impl CompletionCandidate {
@@ -688,8 +705,8 @@ impl CompletionCandidate {
     }
 
     /// Set the visibility of the completion candidate
-    pub fn visible(mut self, visible: bool) -> Self {
-        self.visible = visible;
+    pub fn hide(mut self, hidden: bool) -> Self {
+        self.hidden = hidden;
         self
     }
 
@@ -704,7 +721,65 @@ impl CompletionCandidate {
     }
 
     /// Get the visibility of the completion candidate
-    pub fn is_visible(&self) -> bool {
-        self.visible
+    pub fn is_hide_set(&self) -> bool {
+        self.hidden
     }
 }
+
+/// User-provided completion candidates for an argument.
+///
+/// This is useful when predefined value hints are not enough.
+pub trait CustomCompleter: Send + Sync {
+    /// All potential candidates for an argument.
+    ///
+    /// See [`CompletionCandidate`] for more information.
+    fn completions(&self) -> Vec<CompletionCandidate>;
+}
+
+impl<F> CustomCompleter for F
+where
+    F: Fn() -> Vec<CompletionCandidate> + Send + Sync,
+{
+    fn completions(&self) -> Vec<CompletionCandidate> {
+        self()
+    }
+}
+
+/// A wrapper for custom completer
+///
+/// # Example
+///
+/// ```rust
+/// use clap::Parser;
+/// use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate};
+///
+/// #[derive(Debug, Parser)]
+/// struct Cli {
+///     #[arg(long, add = ArgValueCompleter::new(|| { vec![
+///         CompletionCandidate::new("foo"),
+///         CompletionCandidate::new("bar"),
+///         CompletionCandidate::new("baz")] }))]
+///     custom: Option<String>,
+/// }
+///    
+/// ```
+#[derive(Clone)]
+pub struct ArgValueCompleter(Arc<dyn CustomCompleter>);
+
+impl ArgValueCompleter {
+    /// Create a new `ArgValueCompleter` with a custom completer
+    pub fn new<C: CustomCompleter>(completer: C) -> Self
+    where
+        C: 'static + CustomCompleter,
+    {
+        Self(Arc::new(completer))
+    }
+}
+
+impl std::fmt::Debug for ArgValueCompleter {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(type_name::<Self>())
+    }
+}
+
+impl ArgExt for ArgValueCompleter {}
diff --git a/examples/cargo-example-derive.rs b/examples/cargo-example-derive.rs
index 80ad93fa..f9105ea2 100644
--- a/examples/cargo-example-derive.rs
+++ b/examples/cargo-example-derive.rs
@@ -3,21 +3,10 @@ use clap::Parser;
 #[derive(Parser)] // requires `derive` feature
 #[command(name = "cargo")]
 #[command(bin_name = "cargo")]
-#[command(styles = CLAP_STYLING)]
 enum CargoCli {
     ExampleDerive(ExampleDeriveArgs),
 }
 
-// See also `clap_cargo::style::CLAP_STYLING`
-pub const CLAP_STYLING: clap::builder::styling::Styles = clap::builder::styling::Styles::styled()
-    .header(clap_cargo::style::HEADER)
-    .usage(clap_cargo::style::USAGE)
-    .literal(clap_cargo::style::LITERAL)
-    .placeholder(clap_cargo::style::PLACEHOLDER)
-    .error(clap_cargo::style::ERROR)
-    .valid(clap_cargo::style::VALID)
-    .invalid(clap_cargo::style::INVALID);
-
 #[derive(clap::Args)]
 #[command(version, about, long_about = None)]
 struct ExampleDeriveArgs {
diff --git a/examples/cargo-example.rs b/examples/cargo-example.rs
index ecf74e58..11733651 100644
--- a/examples/cargo-example.rs
+++ b/examples/cargo-example.rs
@@ -1,7 +1,6 @@
 fn main() {
     let cmd = clap::Command::new("cargo")
         .bin_name("cargo")
-        .styles(CLAP_STYLING)
         .subcommand_required(true)
         .subcommand(
             clap::command!("example").arg(
@@ -17,13 +16,3 @@ fn main() {
     let manifest_path = matches.get_one::<std::path::PathBuf>("manifest-path");
     println!("{manifest_path:?}");
 }
-
-// See also `clap_cargo::style::CLAP_STYLING`
-pub const CLAP_STYLING: clap::builder::styling::Styles = clap::builder::styling::Styles::styled()
-    .header(clap_cargo::style::HEADER)
-    .usage(clap_cargo::style::USAGE)
-    .literal(clap_cargo::style::LITERAL)
-    .placeholder(clap_cargo::style::PLACEHOLDER)
-    .error(clap_cargo::style::ERROR)
-    .valid(clap_cargo::style::VALID)
-    .invalid(clap_cargo::style::INVALID);
diff --git a/src/_cookbook/mod.rs b/src/_cookbook/mod.rs
index 364e43b1..9753d37a 100644
--- a/src/_cookbook/mod.rs
+++ b/src/_cookbook/mod.rs
@@ -15,7 +15,6 @@
 //! - Topics:
 //!   - Subcommands
 //!   - Cargo plugins
-//!   - custom terminal [styles][crate::Command::styles] (colors)
 //!
 //! find-like interface: [builder][find]
 //! - Topics:
diff --git a/src/bin/stdio-fixture.rs b/src/bin/stdio-fixture.rs
index 03ecd1fd..efd6c817 100644
--- a/src/bin/stdio-fixture.rs
+++ b/src/bin/stdio-fixture.rs
@@ -15,12 +15,12 @@ fn main() {
     #[cfg(feature = "color")]
     {
         use clap::builder::styling;
-        const STYLES: styling::Styles = styling::Styles::styled()
-            .header(styling::AnsiColor::Green.on_default().bold())
-            .usage(styling::AnsiColor::Green.on_default().bold())
-            .literal(styling::AnsiColor::Blue.on_default().bold())
+        let styles = styling::Styles::styled()
+            .header(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
+            .usage(styling::AnsiColor::Green.on_default() | styling::Effects::BOLD)
+            .literal(styling::AnsiColor::Blue.on_default() | styling::Effects::BOLD)
             .placeholder(styling::AnsiColor::Cyan.on_default());
-        cmd = cmd.styles(STYLES);
+        cmd = cmd.styles(styles);
     }
     cmd.get_matches();
 }


Test patch:
diff --git a/clap_complete/tests/testsuite/dynamic.rs b/clap_complete/tests/testsuite/dynamic.rs
index 0c0fe22b..31214fa1 100644
--- a/clap_complete/tests/testsuite/dynamic.rs
+++ b/clap_complete/tests/testsuite/dynamic.rs
@@ -4,6 +4,7 @@ use std::fs;
 use std::path::Path;
 
 use clap::{builder::PossibleValue, Command};
+use clap_complete::dynamic::{ArgValueCompleter, CompletionCandidate, CustomCompleter};
 use snapbox::assert_data_eq;
 
 macro_rules! complete {
@@ -590,6 +591,37 @@ val3
     );
 }
 
+#[test]
+fn suggest_custom_arg_value() {
+    #[derive(Debug)]
+    struct MyCustomCompleter {}
+
+    impl CustomCompleter for MyCustomCompleter {
+        fn completions(&self) -> Vec<CompletionCandidate> {
+            vec![
+                CompletionCandidate::new("custom1"),
+                CompletionCandidate::new("custom2"),
+                CompletionCandidate::new("custom3"),
+            ]
+        }
+    }
+
+    let mut cmd = Command::new("dynamic").arg(
+        clap::Arg::new("custom")
+            .long("custom")
+            .add::<ArgValueCompleter>(ArgValueCompleter::new(MyCustomCompleter {})),
+    );
+
+    assert_data_eq!(
+        complete!(cmd, "--custom [TAB]"),
+        snapbox::str![
+            "custom1
+custom2
+custom3"
+        ],
+    );
+}
+
 #[test]
 fn suggest_multi_positional() {
     let mut cmd = Command::new("dynamic")