Skip to content

Commit d7373e0

Browse files
committed
feat(complete): support flag-style subcommands in completion engine
1 parent 3139060 commit d7373e0

2 files changed

Lines changed: 134 additions & 11 deletions

File tree

clap_complete/src/engine/complete.rs

Lines changed: 123 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ pub fn complete(
7777
}
7878
} else if let Some((flag, value)) = arg.to_long() {
7979
if let Ok(flag) = flag {
80+
if value.is_none() {
81+
if let Some(subcmd) = find_long_flag_subcmd(current_cmd, flag) {
82+
current_cmd = subcmd;
83+
pos_index = 1;
84+
continue;
85+
}
86+
}
87+
8088
let opt = current_cmd.get_arguments().find(|a| {
8189
let longs = a.get_long_and_visible_aliases();
8290
let is_find = longs.map(|v| {
@@ -96,15 +104,39 @@ pub fn complete(
96104
parse_positional(current_cmd, pos_index, is_escaped, current_state);
97105
}
98106
}
99-
} else if let Some(short) = arg.to_short() {
100-
let (_, takes_value_opt, mut short) = parse_shortflags(current_cmd, short);
101-
if let Some(opt) = takes_value_opt {
102-
if short.next_value_os().is_none() {
103-
next_state = ParseState::Opt((opt, 1));
107+
} else if let Some(mut short) = arg.to_short() {
108+
// Check if the first short flag is a subcommand flag.
109+
let mut found_subcmd = false;
110+
let mut peek = short.clone();
111+
if let Some(Ok(c)) = peek.next_flag() {
112+
if let Some(subcmd) = find_short_flag_subcmd(current_cmd, c) {
113+
current_cmd = subcmd;
114+
pos_index = 1;
115+
found_subcmd = true;
116+
// Consume the subcommand flag from the real iterator.
117+
short.next_flag();
118+
// If there are remaining flags, parse them as flags of the subcommand.
119+
if peek.next_flag().is_some() {
120+
let (_, takes_value_opt, remaining) = parse_shortflags(current_cmd, short);
121+
short = remaining;
122+
if let Some(opt) = takes_value_opt {
123+
if short.next_value_os().is_none() {
124+
next_state = ParseState::Opt((opt, 1));
125+
}
126+
}
127+
}
128+
}
129+
}
130+
if !found_subcmd {
131+
let (_, takes_value_opt, mut short) = parse_shortflags(current_cmd, short);
132+
if let Some(opt) = takes_value_opt {
133+
if short.next_value_os().is_none() {
134+
next_state = ParseState::Opt((opt, 1));
135+
}
136+
} else if pos_allows_hyphen(current_cmd, pos_index) {
137+
(next_state, pos_index) =
138+
parse_positional(current_cmd, pos_index, is_escaped, current_state);
104139
}
105-
} else if pos_allows_hyphen(current_cmd, pos_index) {
106-
(next_state, pos_index) =
107-
parse_positional(current_cmd, pos_index, is_escaped, current_state);
108140
}
109141
} else {
110142
match current_state {
@@ -285,10 +317,55 @@ fn complete_option(
285317
.into_iter()
286318
.filter(|comp| comp.get_value().starts_with(format!("--{flag}").as_str())),
287319
);
320+
completions.extend(
321+
long_flag_subcommands(cmd)
322+
.into_iter()
323+
.filter(|comp| comp.get_value().starts_with(format!("--{flag}").as_str())),
324+
);
288325
}
289326
}
290327
} else if let Some(short) = arg.to_short() {
291328
if !short.is_negative_number() {
329+
// Check if the first short flag is a subcommand flag.
330+
let mut peek = short.clone();
331+
if let Some(Ok(c)) = peek.next_flag() {
332+
if let Some(subcmd) = find_short_flag_subcmd(cmd, c) {
333+
// First flag is a subcommand; complete remaining flags from the subcommand.
334+
let mut inner_short = short.clone();
335+
inner_short.next_flag(); // consume the subcommand flag
336+
let subcmd_prefix = format!("-{c}");
337+
338+
let (leading_flags, takes_value_opt, mut remaining) =
339+
parse_shortflags(subcmd, inner_short);
340+
341+
if let Some(opt) = takes_value_opt {
342+
let mut peek_remaining = remaining.clone();
343+
let has_equal = if let Some(Ok('=')) = peek_remaining.next_flag() {
344+
remaining.next_flag();
345+
true
346+
} else {
347+
false
348+
};
349+
350+
let value = remaining.next_value_os().unwrap_or(OsStr::new(""));
351+
completions.extend(
352+
complete_arg_value(value.to_str().ok_or(value), opt, current_dir)
353+
.into_iter()
354+
.map(|comp| {
355+
let sep = if has_equal { "=" } else { "" };
356+
comp.add_prefix(format!("{subcmd_prefix}{leading_flags}{sep}"))
357+
}),
358+
);
359+
} else {
360+
completions.extend(shorts_and_visible_aliases(subcmd).into_iter().map(
361+
|comp| comp.add_prefix(format!("{subcmd_prefix}{leading_flags}")),
362+
));
363+
}
364+
365+
return completions;
366+
}
367+
}
368+
292369
// Find the first takes_values option.
293370
let (leading_flags, takes_value_opt, mut short) = parse_shortflags(cmd, short);
294371

@@ -503,6 +580,30 @@ fn hidden_longs_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
503580
.collect()
504581
}
505582

583+
/// Gets long flag subcommands as `--flag` style completion candidates.
584+
fn long_flag_subcommands(cmd: &clap::Command) -> Vec<CompletionCandidate> {
585+
cmd.get_subcommands()
586+
.flat_map(|sc| {
587+
let mut candidates = Vec::new();
588+
if let Some(long_flag) = sc.get_long_flag() {
589+
candidates.push(populate_command_candidate(
590+
CompletionCandidate::new(format!("--{long_flag}")),
591+
cmd,
592+
sc,
593+
));
594+
}
595+
for alias in sc.get_visible_long_flag_aliases() {
596+
candidates.push(populate_command_candidate(
597+
CompletionCandidate::new(format!("--{alias}")),
598+
cmd,
599+
sc,
600+
));
601+
}
602+
candidates
603+
})
604+
.collect()
605+
}
606+
506607
/// Gets all the short options, their visible aliases and flags of a [`clap::Command`].
507608
/// Includes `h` and `V` depending on the [`clap::Command`] settings.
508609
fn shorts_and_visible_aliases(p: &clap::Command) -> Vec<CompletionCandidate> {
@@ -705,3 +806,17 @@ fn opt_allows_hyphen(state: &ParseState<'_>, arg: &clap_lex::ParsedArg<'_>) -> b
705806

706807
false
707808
}
809+
810+
/// Find a subcommand by short flag (including aliases).
811+
fn find_short_flag_subcmd(cmd: &clap::Command, flag: char) -> Option<&clap::Command> {
812+
cmd.get_subcommands().find(|sc| {
813+
sc.get_short_flag() == Some(flag) || sc.get_all_short_flag_aliases().any(|a| a == flag)
814+
})
815+
}
816+
817+
/// Find a subcommand by long flag (including aliases).
818+
fn find_long_flag_subcmd<'c>(cmd: &'c clap::Command, flag: &str) -> Option<&'c clap::Command> {
819+
cmd.get_subcommands().find(|sc| {
820+
sc.get_long_flag() == Some(flag) || sc.get_all_long_flag_aliases().any(|a| a == flag)
821+
})
822+
}

clap_complete/tests/testsuite/engine.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1479,19 +1479,27 @@ fn suggest_flag_subcommand_completion() {
14791479
// Completing long flag subcommands
14801480
assert_data_eq!(
14811481
complete!(cmd, "--syn[TAB]"),
1482-
snapbox::str![""]
1482+
snapbox::str!["--sync"]
14831483
);
14841484

14851485
// After selecting a short flag subcommand, complete its flags
14861486
assert_data_eq!(
14871487
complete!(cmd, "-S --[TAB]"),
1488-
snapbox::str!["--help Print help"]
1488+
snapbox::str![[r#"
1489+
--search
1490+
--quiet
1491+
--help Print help
1492+
"#]]
14891493
);
14901494

14911495
// Combined short flags after flag subcommand: -Ss (sync + search)
14921496
assert_data_eq!(
14931497
complete!(cmd, "-Ss[TAB]"),
1494-
snapbox::str!["-Ssh Print help"]
1498+
snapbox::str![[r#"
1499+
-Sss --search
1500+
-Ssq --quiet
1501+
-Ssh Print help
1502+
"#]]
14951503
);
14961504
}
14971505

0 commit comments

Comments
 (0)