Skip to content

Commit 02244a6

Browse files
authored
Merge pull request #5949 from krobelus/option-name-completions-after-positionals
Fix arbitrary-cardinality positionals shadowing option name completion
2 parents 352e99f + 2e13847 commit 02244a6

File tree

2 files changed

+190
-109
lines changed

2 files changed

+190
-109
lines changed

clap_complete/src/engine/complete.rs

Lines changed: 106 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -157,100 +157,20 @@ fn complete_arg(
157157
{
158158
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
159159
}
160-
161-
if arg.is_empty() {
162-
completions.extend(longs_and_visible_aliases(cmd));
163-
completions.extend(hidden_longs_aliases(cmd));
164-
165-
let dash_or_arg = if arg.is_empty() {
166-
"-".into()
167-
} else {
168-
arg.to_value_os().to_string_lossy()
169-
};
170-
completions.extend(
171-
shorts_and_visible_aliases(cmd)
172-
.into_iter()
173-
.map(|comp| comp.add_prefix(dash_or_arg.to_string())),
174-
);
175-
} else if arg.is_stdio() {
176-
// HACK: Assuming knowledge of is_stdio
177-
let dash_or_arg = if arg.is_empty() {
178-
"-".into()
179-
} else {
180-
arg.to_value_os().to_string_lossy()
181-
};
182-
completions.extend(
183-
shorts_and_visible_aliases(cmd)
184-
.into_iter()
185-
.map(|comp| comp.add_prefix(dash_or_arg.to_string())),
186-
);
187-
188-
completions.extend(longs_and_visible_aliases(cmd));
189-
completions.extend(hidden_longs_aliases(cmd));
190-
} else if arg.is_escape() {
191-
// HACK: Assuming knowledge of is_escape
192-
completions.extend(longs_and_visible_aliases(cmd));
193-
completions.extend(hidden_longs_aliases(cmd));
194-
} else if let Some((flag, value)) = arg.to_long() {
195-
if let Ok(flag) = flag {
196-
if let Some(value) = value {
197-
if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag))
198-
{
199-
completions.extend(
200-
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
201-
.into_iter()
202-
.map(|comp| comp.add_prefix(format!("--{flag}="))),
203-
);
204-
}
205-
} else {
206-
completions.extend(longs_and_visible_aliases(cmd).into_iter().filter(
207-
|comp| comp.get_value().starts_with(format!("--{flag}").as_str()),
208-
));
209-
completions.extend(hidden_longs_aliases(cmd).into_iter().filter(|comp| {
210-
comp.get_value().starts_with(format!("--{flag}").as_str())
211-
}));
212-
}
213-
}
214-
} else if let Some(short) = arg.to_short() {
215-
if !short.is_negative_number() {
216-
// Find the first takes_values option.
217-
let (leading_flags, takes_value_opt, mut short) = parse_shortflags(cmd, short);
218-
219-
// Clone `short` to `peek_short` to peek whether the next flag is a `=`.
220-
if let Some(opt) = takes_value_opt {
221-
let mut peek_short = short.clone();
222-
let has_equal = if let Some(Ok('=')) = peek_short.next_flag() {
223-
short.next_flag();
224-
true
225-
} else {
226-
false
227-
};
228-
229-
let value = short.next_value_os().unwrap_or(OsStr::new(""));
230-
completions.extend(
231-
complete_arg_value(value.to_str().ok_or(value), opt, current_dir)
232-
.into_iter()
233-
.map(|comp| {
234-
let sep = if has_equal { "=" } else { "" };
235-
comp.add_prefix(format!("-{leading_flags}{sep}"))
236-
}),
237-
);
238-
} else {
239-
completions.extend(
240-
shorts_and_visible_aliases(cmd)
241-
.into_iter()
242-
.map(|comp| comp.add_prefix(format!("-{leading_flags}"))),
243-
);
244-
}
245-
}
246-
}
160+
completions.extend(complete_option(arg, cmd, current_dir));
247161
}
248-
ParseState::Pos(..) => {
162+
ParseState::Pos((_, num_arg)) => {
249163
if let Some(positional) = cmd
250164
.get_positionals()
251165
.find(|p| p.get_index() == Some(pos_index))
252166
{
253167
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
168+
if positional
169+
.get_num_args()
170+
.is_some_and(|num_args| num_arg >= num_args.min_values())
171+
{
172+
completions.extend(complete_option(arg, cmd, current_dir));
173+
}
254174
}
255175
}
256176
ParseState::Opt((opt, count)) => {
@@ -297,6 +217,104 @@ fn complete_arg(
297217
Ok(completions)
298218
}
299219

220+
fn complete_option(
221+
arg: &clap_lex::ParsedArg<'_>,
222+
cmd: &clap::Command,
223+
current_dir: Option<&std::path::Path>,
224+
) -> Vec<CompletionCandidate> {
225+
let mut completions = Vec::<CompletionCandidate>::new();
226+
if arg.is_empty() {
227+
completions.extend(longs_and_visible_aliases(cmd));
228+
completions.extend(hidden_longs_aliases(cmd));
229+
230+
let dash_or_arg = if arg.is_empty() {
231+
"-".into()
232+
} else {
233+
arg.to_value_os().to_string_lossy()
234+
};
235+
completions.extend(
236+
shorts_and_visible_aliases(cmd)
237+
.into_iter()
238+
.map(|comp| comp.add_prefix(dash_or_arg.to_string())),
239+
);
240+
} else if arg.is_stdio() {
241+
// HACK: Assuming knowledge of is_stdio
242+
let dash_or_arg = if arg.is_empty() {
243+
"-".into()
244+
} else {
245+
arg.to_value_os().to_string_lossy()
246+
};
247+
completions.extend(
248+
shorts_and_visible_aliases(cmd)
249+
.into_iter()
250+
.map(|comp| comp.add_prefix(dash_or_arg.to_string())),
251+
);
252+
253+
completions.extend(longs_and_visible_aliases(cmd));
254+
completions.extend(hidden_longs_aliases(cmd));
255+
} else if arg.is_escape() {
256+
// HACK: Assuming knowledge of is_escape
257+
completions.extend(longs_and_visible_aliases(cmd));
258+
completions.extend(hidden_longs_aliases(cmd));
259+
} else if let Some((flag, value)) = arg.to_long() {
260+
if let Ok(flag) = flag {
261+
if let Some(value) = value {
262+
if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) {
263+
completions.extend(
264+
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
265+
.into_iter()
266+
.map(|comp| comp.add_prefix(format!("--{flag}="))),
267+
);
268+
}
269+
} else {
270+
completions.extend(
271+
longs_and_visible_aliases(cmd)
272+
.into_iter()
273+
.filter(|comp| comp.get_value().starts_with(format!("--{flag}").as_str())),
274+
);
275+
completions.extend(
276+
hidden_longs_aliases(cmd)
277+
.into_iter()
278+
.filter(|comp| comp.get_value().starts_with(format!("--{flag}").as_str())),
279+
);
280+
}
281+
}
282+
} else if let Some(short) = arg.to_short() {
283+
if !short.is_negative_number() {
284+
// Find the first takes_values option.
285+
let (leading_flags, takes_value_opt, mut short) = parse_shortflags(cmd, short);
286+
287+
// Clone `short` to `peek_short` to peek whether the next flag is a `=`.
288+
if let Some(opt) = takes_value_opt {
289+
let mut peek_short = short.clone();
290+
let has_equal = if let Some(Ok('=')) = peek_short.next_flag() {
291+
short.next_flag();
292+
true
293+
} else {
294+
false
295+
};
296+
297+
let value = short.next_value_os().unwrap_or(OsStr::new(""));
298+
completions.extend(
299+
complete_arg_value(value.to_str().ok_or(value), opt, current_dir)
300+
.into_iter()
301+
.map(|comp| {
302+
let sep = if has_equal { "=" } else { "" };
303+
comp.add_prefix(format!("-{leading_flags}{sep}"))
304+
}),
305+
);
306+
} else {
307+
completions.extend(
308+
shorts_and_visible_aliases(cmd)
309+
.into_iter()
310+
.map(|comp| comp.add_prefix(format!("-{leading_flags}"))),
311+
);
312+
}
313+
}
314+
}
315+
completions
316+
}
317+
300318
fn complete_arg_value(
301319
value: Result<&str, &OsStr>,
302320
arg: &clap::Arg,

clap_complete/tests/testsuite/engine.rs

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -681,13 +681,13 @@ baz
681681
fn suggest_multi_positional() {
682682
let mut cmd = Command::new("dynamic")
683683
.arg(
684-
clap::Arg::new("positional")
685-
.value_parser(["pos_1, pos_2, pos_3"])
684+
clap::Arg::new("positional-1")
685+
.value_parser(["pos_1"])
686686
.index(1),
687687
)
688688
.arg(
689689
clap::Arg::new("positional-2")
690-
.value_parser(["pos_a", "pos_b", "pos_c"])
690+
.value_parser(["pos_2_a", "pos_2_b", "pos_2_c"])
691691
.index(2)
692692
.num_args(3),
693693
)
@@ -701,27 +701,27 @@ fn suggest_multi_positional() {
701701
assert_data_eq!(
702702
complete!(cmd, "pos_1 pos_a [TAB]"),
703703
snapbox::str![[r#"
704-
pos_a
705-
pos_b
706-
pos_c
704+
pos_2_a
705+
pos_2_b
706+
pos_2_c
707707
"#]]
708708
);
709709

710710
assert_data_eq!(
711711
complete!(cmd, "pos_1 pos_a pos_b [TAB]"),
712712
snapbox::str![[r#"
713-
pos_a
714-
pos_b
715-
pos_c
713+
pos_2_a
714+
pos_2_b
715+
pos_2_c
716716
"#]]
717717
);
718718

719719
assert_data_eq!(
720720
complete!(cmd, "--format json pos_1 [TAB]"),
721721
snapbox::str![[r#"
722-
pos_a
723-
pos_b
724-
pos_c
722+
pos_2_a
723+
pos_2_b
724+
pos_2_c
725725
--format
726726
--help Print help
727727
"#]]
@@ -730,9 +730,9 @@ pos_c
730730
assert_data_eq!(
731731
complete!(cmd, "--format json pos_1 pos_a [TAB]"),
732732
snapbox::str![[r#"
733-
pos_a
734-
pos_b
735-
pos_c
733+
pos_2_a
734+
pos_2_b
735+
pos_2_c
736736
"#]]
737737
);
738738

@@ -747,18 +747,18 @@ pos_c
747747
assert_data_eq!(
748748
complete!(cmd, "--format json -- pos_1 pos_a [TAB]"),
749749
snapbox::str![[r#"
750-
pos_a
751-
pos_b
752-
pos_c
750+
pos_2_a
751+
pos_2_b
752+
pos_2_c
753753
"#]]
754754
);
755755

756756
assert_data_eq!(
757757
complete!(cmd, "--format json -- pos_1 pos_a pos_b [TAB]"),
758758
snapbox::str![[r#"
759-
pos_a
760-
pos_b
761-
pos_c
759+
pos_2_a
760+
pos_2_b
761+
pos_2_c
762762
"#]]
763763
);
764764

@@ -768,6 +768,69 @@ pos_c
768768
);
769769
}
770770

771+
#[test]
772+
fn suggest_multi_positional_unbounded() {
773+
let mut cmd = Command::new("dynamic")
774+
.arg(
775+
clap::Arg::new("positional")
776+
.value_parser(["pos_1", "pos_2"])
777+
.index(1)
778+
.num_args(2..),
779+
)
780+
.arg(
781+
clap::Arg::new("--format")
782+
.long("format")
783+
.short('F')
784+
.value_parser(["json", "yaml", "toml"]),
785+
);
786+
787+
assert_data_eq!(
788+
complete!(cmd, "pos_1 [TAB]"),
789+
snapbox::str![[r#"
790+
pos_1
791+
pos_2
792+
"#]]
793+
);
794+
795+
assert_data_eq!(complete!(cmd, "pos_1 --[TAB]"), snapbox::str![""]);
796+
797+
assert_data_eq!(
798+
complete!(cmd, "pos_1 --format [TAB]"),
799+
snapbox::str![[r#"
800+
json
801+
yaml
802+
toml
803+
"#]]
804+
);
805+
806+
assert_data_eq!(
807+
complete!(cmd, "pos_1 --format json [TAB]"),
808+
snapbox::str![[r#"
809+
pos_1
810+
pos_2
811+
--format
812+
--help Print help
813+
"#]]
814+
);
815+
816+
assert_data_eq!(
817+
complete!(cmd, "pos_1 pos_2 --[TAB]"),
818+
snapbox::str![[r#"
819+
--format
820+
--help Print help
821+
"#]]
822+
);
823+
assert_data_eq!(
824+
complete!(cmd, "pos_1 pos_2 --format json [TAB]"),
825+
snapbox::str![[r#"
826+
pos_1
827+
pos_2
828+
--format
829+
--help Print help
830+
"#]]
831+
);
832+
}
833+
771834
#[test]
772835
fn suggest_delimiter_values() {
773836
let mut cmd = Command::new("delimiter")

0 commit comments

Comments
 (0)