Skip to content

Commit a271c19

Browse files
authored
Merge pull request #5539 from shannmu/option_value
feat(clap_complete): Support flags with values `--flag bar`and `-f bar` in native completions
2 parents 73b07c9 + f7383f7 commit a271c19

2 files changed

Lines changed: 303 additions & 74 deletions

File tree

clap_complete/src/dynamic/completer.rs

Lines changed: 186 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ pub fn complete(
5353
let mut current_cmd = &*cmd;
5454
let mut pos_index = 1;
5555
let mut is_escaped = false;
56+
let mut state = ParseState::ValueDone;
5657
while let Some(arg) = raw_args.next(&mut cursor) {
5758
if cursor == target_cursor {
58-
return complete_arg(&arg, current_cmd, current_dir, pos_index, is_escaped);
59+
return complete_arg(&arg, current_cmd, current_dir, pos_index, state);
5960
}
6061

6162
debug!("complete::next: Begin parsing '{:?}'", arg.to_value_os(),);
@@ -64,18 +65,103 @@ pub fn complete(
6465
if let Some(next_cmd) = current_cmd.find_subcommand(value) {
6566
current_cmd = next_cmd;
6667
pos_index = 1;
68+
state = ParseState::ValueDone;
6769
continue;
6870
}
6971
}
7072

7173
if is_escaped {
7274
pos_index += 1;
75+
state = ParseState::Pos(pos_index);
7376
} else if arg.is_escape() {
7477
is_escaped = true;
75-
} else if let Some(_long) = arg.to_long() {
76-
} else if let Some(_short) = arg.to_short() {
78+
state = ParseState::ValueDone;
79+
} else if let Some((flag, value)) = arg.to_long() {
80+
if let Ok(flag) = flag {
81+
let opt = current_cmd.get_arguments().find(|a| {
82+
let longs = a.get_long_and_visible_aliases();
83+
let is_find = longs.map(|v| {
84+
let mut iter = v.into_iter();
85+
let s = iter.find(|s| *s == flag);
86+
s.is_some()
87+
});
88+
is_find.unwrap_or(false)
89+
});
90+
state = match opt.map(|o| o.get_action()) {
91+
Some(clap::ArgAction::Set) | Some(clap::ArgAction::Append) => {
92+
if value.is_some() {
93+
ParseState::ValueDone
94+
} else {
95+
ParseState::Opt(opt.unwrap().clone())
96+
}
97+
}
98+
Some(clap::ArgAction::SetTrue) | Some(clap::ArgAction::SetFalse) => {
99+
ParseState::ValueDone
100+
}
101+
Some(clap::ArgAction::Count) => ParseState::ValueDone,
102+
Some(clap::ArgAction::Version) => ParseState::ValueDone,
103+
Some(clap::ArgAction::Help)
104+
| Some(clap::ArgAction::HelpLong)
105+
| Some(clap::ArgAction::HelpShort) => ParseState::ValueDone,
106+
Some(_) => ParseState::ValueDone,
107+
None => ParseState::ValueDone,
108+
};
109+
} else {
110+
state = ParseState::ValueDone;
111+
}
112+
} else if let Some(mut short) = arg.to_short() {
113+
let mut takes_value = false;
114+
loop {
115+
if let Some(Ok(opt)) = short.next_flag() {
116+
let opt = current_cmd.get_arguments().find(|a| {
117+
let shorts = a.get_short_and_visible_aliases();
118+
let is_find = shorts.map(|v| {
119+
let mut iter = v.into_iter();
120+
let c = iter.find(|c| *c == opt);
121+
c.is_some()
122+
});
123+
is_find.unwrap_or(false)
124+
});
125+
126+
state = match opt.map(|o| o.get_action()) {
127+
Some(clap::ArgAction::Set) | Some(clap::ArgAction::Append) => {
128+
takes_value = true;
129+
if short.next_value_os().is_some() {
130+
ParseState::ValueDone
131+
} else {
132+
ParseState::Opt(opt.unwrap().clone())
133+
}
134+
}
135+
Some(clap::ArgAction::SetTrue) | Some(clap::ArgAction::SetFalse) => {
136+
ParseState::ValueDone
137+
}
138+
Some(clap::ArgAction::Count) => ParseState::ValueDone,
139+
Some(clap::ArgAction::Version) => ParseState::ValueDone,
140+
Some(clap::ArgAction::Help)
141+
| Some(clap::ArgAction::HelpShort)
142+
| Some(clap::ArgAction::HelpLong) => ParseState::ValueDone,
143+
Some(_) => ParseState::ValueDone,
144+
None => ParseState::ValueDone,
145+
};
146+
147+
if takes_value {
148+
break;
149+
}
150+
} else {
151+
state = ParseState::ValueDone;
152+
break;
153+
}
154+
}
77155
} else {
78-
pos_index += 1;
156+
match state {
157+
ParseState::ValueDone | ParseState::Pos(_) => {
158+
pos_index += 1;
159+
state = ParseState::ValueDone;
160+
}
161+
ParseState::Opt(_) => {
162+
state = ParseState::ValueDone;
163+
}
164+
}
79165
}
80166
}
81167

@@ -85,96 +171,123 @@ pub fn complete(
85171
))
86172
}
87173

174+
#[derive(Debug, PartialEq, Eq, Clone)]
175+
enum ParseState {
176+
/// Parsing a value done, there is no state to record.
177+
ValueDone,
178+
179+
/// Parsing a positional argument after `--`
180+
Pos(usize),
181+
182+
/// Parsing a optional flag argument
183+
Opt(clap::Arg),
184+
}
185+
88186
fn complete_arg(
89187
arg: &clap_lex::ParsedArg<'_>,
90188
cmd: &clap::Command,
91189
current_dir: Option<&std::path::Path>,
92190
pos_index: usize,
93-
is_escaped: bool,
191+
state: ParseState,
94192
) -> Result<Vec<CompletionCandidate>, std::io::Error> {
95193
debug!(
96-
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={}, is_escaped={}",
194+
"complete_arg: arg={:?}, cmd={:?}, current_dir={:?}, pos_index={:?}, state={:?}",
97195
arg,
98196
cmd.get_name(),
99197
current_dir,
100198
pos_index,
101-
is_escaped
199+
state
102200
);
103201
let mut completions = Vec::<CompletionCandidate>::new();
104202

105-
if !is_escaped {
106-
if let Some((flag, value)) = arg.to_long() {
107-
if let Ok(flag) = flag {
108-
if let Some(value) = value {
109-
if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag)) {
110-
completions.extend(
111-
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
112-
.into_iter()
113-
.map(|comp| {
114-
CompletionCandidate::new(format!(
115-
"--{}={}",
116-
flag,
117-
comp.get_content().to_string_lossy()
118-
))
119-
.help(comp.get_help().cloned())
120-
.visible(comp.is_visible())
121-
}),
122-
);
203+
match state {
204+
ParseState::ValueDone => {
205+
if let Some((flag, value)) = arg.to_long() {
206+
if let Ok(flag) = flag {
207+
if let Some(value) = value {
208+
if let Some(arg) = cmd.get_arguments().find(|a| a.get_long() == Some(flag))
209+
{
210+
completions.extend(
211+
complete_arg_value(value.to_str().ok_or(value), arg, current_dir)
212+
.into_iter()
213+
.map(|comp| {
214+
CompletionCandidate::new(format!(
215+
"--{}={}",
216+
flag,
217+
comp.get_content().to_string_lossy()
218+
))
219+
.help(comp.get_help().cloned())
220+
.visible(comp.is_visible())
221+
}),
222+
);
223+
}
224+
} else {
225+
completions.extend(longs_and_visible_aliases(cmd).into_iter().filter(
226+
|comp| {
227+
comp.get_content()
228+
.starts_with(format!("--{}", flag).as_str())
229+
},
230+
));
231+
232+
completions.extend(hidden_longs_aliases(cmd).into_iter().filter(|comp| {
233+
comp.get_content()
234+
.starts_with(format!("--{}", flag).as_str())
235+
}))
123236
}
124-
} else {
125-
completions.extend(longs_and_visible_aliases(cmd).into_iter().filter(|comp| {
126-
comp.get_content()
127-
.starts_with(format!("--{}", flag).as_str())
128-
}));
129-
130-
completions.extend(hidden_longs_aliases(cmd).into_iter().filter(|comp| {
131-
comp.get_content()
132-
.starts_with(format!("--{}", flag).as_str())
133-
}))
134237
}
135-
}
136-
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
137-
// HACK: Assuming knowledge of is_escape / is_stdio
138-
completions.extend(longs_and_visible_aliases(cmd));
238+
} else if arg.is_escape() || arg.is_stdio() || arg.is_empty() {
239+
// HACK: Assuming knowledge of is_escape / is_stdio
240+
completions.extend(longs_and_visible_aliases(cmd));
139241

140-
completions.extend(hidden_longs_aliases(cmd));
141-
}
242+
completions.extend(hidden_longs_aliases(cmd));
243+
}
142244

143-
if arg.is_empty() || arg.is_stdio() || arg.is_short() {
144-
let dash_or_arg = if arg.is_empty() {
145-
"-".into()
146-
} else {
147-
arg.to_value_os().to_string_lossy()
148-
};
149-
// HACK: Assuming knowledge of is_stdio
150-
completions.extend(
151-
shorts_and_visible_aliases(cmd)
152-
.into_iter()
153-
// HACK: Need better `OsStr` manipulation
154-
.map(|comp| {
155-
CompletionCandidate::new(format!(
156-
"{}{}",
157-
dash_or_arg,
158-
comp.get_content().to_string_lossy()
159-
))
160-
.help(comp.get_help().cloned())
161-
.visible(true)
162-
}),
163-
);
164-
}
165-
}
245+
if arg.is_empty() || arg.is_stdio() || arg.is_short() {
246+
let dash_or_arg = if arg.is_empty() {
247+
"-".into()
248+
} else {
249+
arg.to_value_os().to_string_lossy()
250+
};
251+
// HACK: Assuming knowledge of is_stdio
252+
completions.extend(
253+
shorts_and_visible_aliases(cmd)
254+
.into_iter()
255+
// HACK: Need better `OsStr` manipulation
256+
.map(|comp| {
257+
CompletionCandidate::new(format!(
258+
"{}{}",
259+
dash_or_arg,
260+
comp.get_content().to_string_lossy()
261+
))
262+
.help(comp.get_help().cloned())
263+
.visible(true)
264+
}),
265+
);
266+
}
166267

167-
if let Some(positional) = cmd
168-
.get_positionals()
169-
.find(|p| p.get_index() == Some(pos_index))
170-
{
171-
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir).into_iter());
172-
}
268+
if let Some(positional) = cmd
269+
.get_positionals()
270+
.find(|p| p.get_index() == Some(pos_index))
271+
{
272+
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
273+
}
173274

174-
if let Ok(value) = arg.to_value() {
175-
completions.extend(complete_subcommand(value, cmd));
275+
if let Ok(value) = arg.to_value() {
276+
completions.extend(complete_subcommand(value, cmd));
277+
}
278+
}
279+
ParseState::Pos(_) => {
280+
if let Some(positional) = cmd
281+
.get_positionals()
282+
.find(|p| p.get_index() == Some(pos_index))
283+
{
284+
completions.extend(complete_arg_value(arg.to_value(), positional, current_dir));
285+
}
286+
}
287+
ParseState::Opt(opt) => {
288+
completions.extend(complete_arg_value(arg.to_value(), &opt, current_dir));
289+
}
176290
}
177-
178291
if completions.iter().any(|a| a.is_visible()) {
179292
completions.retain(|a| a.is_visible())
180293
}

0 commit comments

Comments
 (0)