Repository: clap-rs/clap
Pull request #5740 (2024-09-20 16:08:08)

fix(complete): De-duplicate built-in candidates
Part of #5058


Patch:
diff --git a/clap_complete/src/engine/candidate.rs b/clap_complete/src/engine/candidate.rs
index c75b46eb..6edf7d2c 100644
--- a/clap_complete/src/engine/candidate.rs
+++ b/clap_complete/src/engine/candidate.rs
@@ -8,6 +8,7 @@ use clap::builder::StyledStr;
 pub struct CompletionCandidate {
     value: OsString,
     help: Option<StyledStr>,
+    id: Option<String>,
     hidden: bool,
 }
 
@@ -27,6 +28,14 @@ impl CompletionCandidate {
         self
     }
 
+    /// Only first for a given Id is shown
+    ///
+    /// To reduce the risk of conflicts, this should likely contain a namespace.
+    pub fn id(mut self, id: Option<String>) -> Self {
+        self.id = id;
+        self
+    }
+
     /// Set the visibility of the completion candidate
     ///
     /// Only shown when there is no visible candidate for completing the current argument.
@@ -60,6 +69,11 @@ impl CompletionCandidate {
         self.help.as_ref()
     }
 
+    /// Get the id used for de-duplicating
+    pub fn get_id(&self) -> Option<&String> {
+        self.id.as_ref()
+    }
+
     /// Get the visibility of the completion candidate
     pub fn is_hide_set(&self) -> bool {
         self.hidden
diff --git a/clap_complete/src/engine/complete.rs b/clap_complete/src/engine/complete.rs
index b0eb0754..fb0463e4 100644
--- a/clap_complete/src/engine/complete.rs
+++ b/clap_complete/src/engine/complete.rs
@@ -257,6 +257,14 @@ fn complete_arg(
     if completions.iter().any(|a| !a.is_hide_set()) {
         completions.retain(|a| !a.is_hide_set());
     }
+    let mut seen_ids = std::collections::HashSet::new();
+    completions.retain(move |a| {
+        if let Some(id) = a.get_id().cloned() {
+            seen_ids.insert(id)
+        } else {
+            true
+        }
+    });
 
     Ok(completions)
 }
@@ -388,6 +396,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())
+                        .id(Some(format!("arg::{}", a.get_id())))
                         .hide(a.is_hide_set())
                 })
             })
@@ -406,6 +415,7 @@ fn hidden_longs_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
                 longs.into_iter().map(|s| {
                     CompletionCandidate::new(format!("--{}", s))
                         .help(a.get_help().cloned())
+                        .id(Some(format!("arg::{}", a.get_id())))
                         .hide(true)
                 })
             })
@@ -425,6 +435,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())
+                        .id(Some(format!("arg::{}", a.get_id())))
                         .hide(a.is_hide_set())
                 })
             })
@@ -458,11 +469,13 @@ fn subcommands(p: &clap::Command) -> Vec<CompletionCandidate> {
                 .map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(sc.get_about().cloned())
+                        .id(Some(format!("command::{}", sc.get_name())))
                         .hide(sc.is_hide_set())
                 })
                 .chain(sc.get_aliases().map(|s| {
                     CompletionCandidate::new(s.to_string())
                         .help(sc.get_about().cloned())
+                        .id(Some(format!("command::{}", sc.get_name())))
                         .hide(true)
                 }))
         })


Test patch:
diff --git a/clap_complete/tests/testsuite/bash.rs b/clap_complete/tests/testsuite/bash.rs
index ffafd170..f1580fd7 100644
--- a/clap_complete/tests/testsuite/bash.rs
+++ b/clap_complete/tests/testsuite/bash.rs
@@ -255,8 +255,8 @@ fn complete_dynamic_env_toplevel() {
     let input = "exhaustive \t\t";
     let expected = snapbox::str![[r#"
 % 
-action      help        last        quote       --global    --help      -h          
-alias       hint        pacman      value       --generate  --version   -V          
+action      help        last        quote       --global    --help      
+alias       hint        pacman      value       --generate  --version   
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
@@ -275,10 +275,9 @@ fn complete_dynamic_env_quoted_help() {
     let input = "exhaustive quote \t\t";
     let expected = snapbox::str![[r#"
 % 
-cmd-backslash      cmd-expansions     --single-quotes    --brackets         --help             
-cmd-backticks      cmd-single-quotes  --double-quotes    --expansions       --version          
-cmd-brackets       escape-help        --backticks        --choice           -h                 
-cmd-double-quotes  help               --backslash        --global           -V                 
+cmd-backslash      cmd-double-quotes  escape-help        --double-quotes    --brackets         --global
+cmd-backticks      cmd-expansions     help               --backticks        --expansions       --help
+cmd-brackets       cmd-single-quotes  --single-quotes    --backslash        --choice           --version
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
diff --git a/clap_complete/tests/testsuite/elvish.rs b/clap_complete/tests/testsuite/elvish.rs
index 2dbf9437..99932586 100644
--- a/clap_complete/tests/testsuite/elvish.rs
+++ b/clap_complete/tests/testsuite/elvish.rs
@@ -198,8 +198,8 @@ fn complete_dynamic_env_toplevel() {
     let expected = snapbox::str![[r#"
 % exhaustive --generate
  COMPLETING argument  
---generate  --help     -V  action  help  last    quote
---global    --version  -h  alias   hint  pacman  value
+--generate  --help     action  help  last    quote
+--global    --version  alias   hint  pacman  value
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
@@ -219,10 +219,9 @@ fn complete_dynamic_env_quoted_help() {
     let expected = snapbox::str![[r#"
 % exhaustive quote --backslash
  COMPLETING argument  
---backslash  --double-quotes  --single-quotes  cmd-backslash      cmd-expansions   
---backticks  --expansions     --version        cmd-backticks      cmd-single-quotes
---brackets   --global         -V               cmd-brackets       escape-help      
---choice     --help           -h               cmd-double-quotes  help             
+--backslash  --choice         --global         --version      cmd-brackets       cmd-single-quotes
+--backticks  --double-quotes  --help           cmd-backslash  cmd-double-quotes  escape-help      
+--brackets   --expansions     --single-quotes  cmd-backticks  cmd-expansions     help             
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
diff --git a/clap_complete/tests/testsuite/engine.rs b/clap_complete/tests/testsuite/engine.rs
index f3539856..8659cc38 100644
--- a/clap_complete/tests/testsuite/engine.rs
+++ b/clap_complete/tests/testsuite/engine.rs
@@ -75,19 +75,12 @@ fn suggest_hidden_subcommand_and_aliases() {
 
     assert_data_eq!(
         complete!(cmd, "test"),
-        snapbox::str![[r#"
-test_visible
-test_visible-alias_visible
-"#]]
+        snapbox::str!["test_visible"]
     );
 
     assert_data_eq!(
         complete!(cmd, "test_h"),
-        snapbox::str![[r#"
-test_hidden
-test_hidden-alias_hidden
-test_hidden-alias_visible
-"#]]
+        snapbox::str!["test_hidden"]
     );
 
     assert_data_eq!(
@@ -119,9 +112,7 @@ fn suggest_subcommand_aliases() {
         complete!(cmd, "hello"),
         snapbox::str![[r#"
 hello-moon
-hello-moon-foo
 hello-world
-hello-world-foo
 "#]],
     );
 }
@@ -167,19 +158,12 @@ fn suggest_hidden_long_flag_aliases() {
 
     assert_data_eq!(
         complete!(cmd, "--test"),
-        snapbox::str![[r#"
---test_visible
---test_visible-alias_visible
-"#]]
+        snapbox::str!["--test_visible"]
     );
 
     assert_data_eq!(
         complete!(cmd, "--test_h"),
-        snapbox::str![[r#"
---test_hidden
---test_hidden-alias_visible
---test_hidden-alias_hidden
-"#]]
+        snapbox::str!["--test_hidden"]
     );
 
     assert_data_eq!(
@@ -287,7 +271,6 @@ hello-world	Say hello to the world
 hello-moon
 goodbye-world
 --help	Print help (see more with '--help')
--h	Print help (see more with '--help')
 "#]],
     );
 }
@@ -361,10 +344,6 @@ pos_c
 --stream
 --count
 --help	Print help
--F
--S
--c
--h	Print help
 "#]]
     );
 
@@ -433,9 +412,6 @@ val3
 --certain-num
 --uncertain-num
 --help	Print help
--Y
--N
--h	Print help
 "#]]
     );
 
@@ -457,9 +433,6 @@ val3
 --certain-num
 --uncertain-num
 --help	Print help
--Y
--N
--h	Print help
 "#]]
     );
 
@@ -469,9 +442,6 @@ val3
 --certain-num
 --uncertain-num
 --help	Print help
--Y
--N
--h	Print help
 "#]]
     );
 
@@ -499,9 +469,6 @@ val3
 --certain-num
 --uncertain-num
 --help	Print help
--Y
--N
--h	Print help
 "#]]
     );
 
@@ -523,9 +490,6 @@ val3
 --certain-num
 --uncertain-num
 --help	Print help
--Y
--N
--h	Print help
 "#]]
     );
 
@@ -535,9 +499,6 @@ val3
 --certain-num
 --uncertain-num
 --help	Print help
--Y
--N
--h	Print help
 "#]]
     );
 }
@@ -775,8 +736,6 @@ pos_b
 pos_c
 --format
 --help	Print help
--F
--h	Print help
 "#]]
     );
 
@@ -794,8 +753,6 @@ pos_c
         snapbox::str![[r#"
 --format
 --help	Print help
--F
--h	Print help
 "#]]
     );
 
@@ -935,8 +892,6 @@ b_pos
 c_pos
 --delimiter
 --help	Print help
--D
--h	Print help
 "#]]);
 
     assert_data_eq!(complete!(cmd, " -- a_pos,[TAB]"), snapbox::str![[r#"
@@ -1028,16 +983,12 @@ fn suggest_positional_long_allow_hyphen() {
 pos_b
 --format
 --help	Print help
--F
--h	Print help
 "#]]
     );
     assert_data_eq!(complete!(cmd, "-F --json --pos_a [TAB]"), snapbox::str![[r#"
 pos_b
 --format
 --help	Print help
--F
--h	Print help
 "#]]);
 
     assert_data_eq!(
@@ -1076,15 +1027,11 @@ fn suggest_positional_short_allow_hyphen() {
 pos_b
 --format
 --help	Print help
--F
--h	Print help
 "#]]);
     assert_data_eq!(complete!(cmd, "-F --json -a [TAB]"), snapbox::str![[r#"
 pos_b
 --format
 --help	Print help
--F
--h	Print help
 "#]]);
 
     assert_data_eq!(
@@ -1125,30 +1072,20 @@ pos-a
 pos-b
 pos-c
 --required-flag
---required-flag2
 --optional-flag
---2optional-flag
 --long-flag
 --help	Print help
--r
--o
 -s
--h	Print help
 "#]]
     );
     assert_data_eq!(
         complete!(cmd, "-[TAB]"),
         snapbox::str![[r#"
 --required-flag
---required-flag2
 --optional-flag
---2optional-flag
 --long-flag
 --help	Print help
--r
--o
 -s
--h	Print help
 "#]]
     );
 }
diff --git a/clap_complete/tests/testsuite/fish.rs b/clap_complete/tests/testsuite/fish.rs
index 2e67e06e..47ff82e7 100644
--- a/clap_complete/tests/testsuite/fish.rs
+++ b/clap_complete/tests/testsuite/fish.rs
@@ -192,11 +192,10 @@ fn complete_dynamic_env_toplevel() {
     let input = "exhaustive \t\t";
     let expected = snapbox::str![[r#"
 % exhaustive action 
-action                                                             pacman                  --help        (Print help)
-alias                                                              quote                   --version  (Print version)
-help  (Print this message or the help of the given subcommand(s))  value                   -h            (Print help)
-hint                                                               --global  (everywhere)  -V         (Print version)
-last                                                               --generate  (generate)  
+action                                                             last    --global      (everywhere)
+alias                                                              pacman  --generate      (generate)
+help  (Print this message or the help of the given subcommand(s))  quote   --help        (Print help)
+hint                                                               value   --version  (Print version)
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
@@ -215,16 +214,24 @@ fn complete_dynamic_env_quoted_help() {
     let input = "exhaustive quote \t\t";
     let expected = snapbox::str![[r#"
 % exhaustive quote 
-cmd-backslash                                        (Avoid '/n')  --backticks    (For more information see `echo test`)
-cmd-backticks              (For more information see `echo test`)  --backslash                              (Avoid '/n')
-cmd-brackets                             (List packages [filter])  --brackets                   (List packages [filter])
-cmd-double-quotes           (Can be "always", "auto", or "never")  --expansions  (Execute the shell command with $SHELL)
-cmd-expansions            (Execute the shell command with $SHELL)  --choice                                             
-cmd-single-quotes           (Can be 'always', 'auto', or 'never')  --global                                 (everywhere)
-escape-help                                             (/tab "')  --help          (Print help (see more with '--help'))
-help  (Print this message or the help of the given subcommand(s))  --version                             (Print version)
---single-quotes             (Can be 'always', 'auto', or 'never')  -h              (Print help (see more with '--help'))
---double-quotes             (Can be "always", "auto", or "never")  -V                                    (Print version)
+cmd-backslash                                        (Avoid '/n')
+cmd-backticks              (For more information see `echo test`)
+cmd-brackets                             (List packages [filter])
+cmd-double-quotes           (Can be "always", "auto", or "never")
+cmd-expansions            (Execute the shell command with $SHELL)
+cmd-single-quotes           (Can be 'always', 'auto', or 'never')
+escape-help                                             (/tab "')
+help  (Print this message or the help of the given subcommand(s))
+--single-quotes             (Can be 'always', 'auto', or 'never')
+--double-quotes             (Can be "always", "auto", or "never")
+--backticks                (For more information see `echo test`)
+--backslash                                          (Avoid '/n')
+--brackets                               (List packages [filter])
+--expansions              (Execute the shell command with $SHELL)
+--choice                                                         
+--global                                             (everywhere)
+--help                      (Print help (see more with '--help'))
+--version                                         (Print version)
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
diff --git a/clap_complete/tests/testsuite/zsh.rs b/clap_complete/tests/testsuite/zsh.rs
index 2331ff4d..d052596c 100644
--- a/clap_complete/tests/testsuite/zsh.rs
+++ b/clap_complete/tests/testsuite/zsh.rs
@@ -185,8 +185,8 @@ fn complete_dynamic_env_toplevel() {
     let input = "exhaustive \t\t";
     let expected = snapbox::str![[r#"
 % exhaustive
---generate  --help      -V          action      help        last        quote       
---global    --version   -h          alias       hint        pacman      value       
+--generate  --help      action      help        last        quote       
+--global    --version   alias       hint        pacman      value       
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);
@@ -205,10 +205,9 @@ fn complete_dynamic_env_quoted_help() {
     let input = "exhaustive quote \t\t";
     let expected = snapbox::str![[r#"
 % exhaustive quote
---backslash        --double-quotes    --single-quotes    cmd-backslash      cmd-expansions     
---backticks        --expansions       --version          cmd-backticks      cmd-single-quotes  
---brackets         --global           -V                 cmd-brackets       escape-help        
---choice           --help             -h                 cmd-double-quotes  help               
+--backslash        --choice           --global           --version          cmd-brackets       cmd-single-quotes
+--backticks        --double-quotes    --help             cmd-backslash      cmd-double-quotes  escape-help
+--brackets         --expansions       --single-quotes    cmd-backticks      cmd-expansions     help
 "#]];
     let actual = runtime.complete(input, &term).unwrap();
     assert_data_eq!(actual, expected);