Skip to content

Commit

Permalink
Merge pull request #5628 from mart-mihkel/complete_hyphen
Browse files Browse the repository at this point in the history
Support `allow_hyphen_values` in native completions
  • Loading branch information
epage committed Sep 5, 2024
2 parents 64e3790 + 57b6cb8 commit 6b7aa3d
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 11 deletions.
56 changes: 45 additions & 11 deletions clap_complete/src/engine/complete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ pub fn complete(
parse_positional(current_cmd, pos_index, is_escaped, current_state);
} else if arg.is_escape() {
is_escaped = true;
} else if opt_allows_hyphen(&current_state, &arg) {
match current_state {
ParseState::Opt((opt, count)) => next_state = parse_opt_value(opt, count),
_ => unreachable!("else branch is only reachable in Opt state"),
}
} else if let Some((flag, value)) = arg.to_long() {
if let Ok(flag) = flag {
let opt = current_cmd.get_arguments().find(|a| {
Expand All @@ -69,10 +74,14 @@ pub fn complete(
});
is_find.unwrap_or(false)
});
if opt.map(|o| o.get_action().takes_values()).unwrap_or(false) {
if value.is_none() {
next_state = ParseState::Opt((opt.unwrap(), 1));

if let Some(opt) = opt {
if opt.get_action().takes_values() && value.is_none() {
next_state = ParseState::Opt((opt, 1));
};
} else if pos_allows_hyphen(current_cmd, pos_index) {
(next_state, pos_index) =
parse_positional(current_cmd, pos_index, is_escaped, current_state);
}
}
} else if let Some(short) = arg.to_short() {
Expand All @@ -81,21 +90,17 @@ pub fn complete(
if short.next_value_os().is_none() {
next_state = ParseState::Opt((opt, 1));
}
} else if pos_allows_hyphen(current_cmd, pos_index) {
(next_state, pos_index) =
parse_positional(current_cmd, pos_index, is_escaped, current_state);
}
} else {
match current_state {
ParseState::ValueDone | ParseState::Pos(..) => {
(next_state, pos_index) =
parse_positional(current_cmd, pos_index, is_escaped, current_state);
}

ParseState::Opt((opt, count)) => {
let range = opt.get_num_args().expect("built");
let max = range.max_values();
if count < max {
next_state = ParseState::Opt((opt, count + 1));
}
}
ParseState::Opt((opt, count)) => next_state = parse_opt_value(opt, count),
}
}
}
Expand Down Expand Up @@ -546,3 +551,32 @@ fn parse_positional<'a>(
),
}
}

/// Parse optional flag argument. Return new state
fn parse_opt_value(opt: &clap::Arg, count: usize) -> ParseState<'_> {
let range = opt.get_num_args().expect("built");
let max = range.max_values();
if count < max {
ParseState::Opt((opt, count + 1))
} else {
ParseState::ValueDone
}
}

fn pos_allows_hyphen(cmd: &clap::Command, pos_index: usize) -> bool {
cmd.get_positionals()
.find(|a| a.get_index() == Some(pos_index))
.map(|p| p.is_allow_hyphen_values_set())
.unwrap_or(false)
}

fn opt_allows_hyphen(state: &ParseState<'_>, arg: &clap_lex::ParsedArg<'_>) -> bool {
let val = arg.to_value_os();
if val.starts_with("-") {
if let ParseState::Opt((opt, _)) = state {
return opt.is_allow_hyphen_values_set();
}
}

false
}
161 changes: 161 additions & 0 deletions clap_complete/tests/testsuite/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -981,6 +981,167 @@ a_pos,c_pos"
);
}

#[test]
fn suggest_allow_hyphen() {
let mut cmd = Command::new("exhaustive")
.arg(
clap::Arg::new("format")
.long("format")
.short('F')
.allow_hyphen_values(true)
.value_parser(["--json", "--toml", "--yaml"]),
)
.arg(clap::Arg::new("json").long("json"));

assert_data_eq!(complete!(cmd, "--format --j[TAB]"), snapbox::str!["--json"]);
assert_data_eq!(complete!(cmd, "-F --j[TAB]"), snapbox::str!["--json"]);
assert_data_eq!(complete!(cmd, "--format --t[TAB]"), snapbox::str!["--toml"]);
assert_data_eq!(complete!(cmd, "-F --t[TAB]"), snapbox::str!["--toml"]);

assert_data_eq!(
complete!(cmd, "--format --[TAB]"),
snapbox::str![
"--json
--toml
--yaml"
]
);

assert_data_eq!(
complete!(cmd, "-F --[TAB]"),
snapbox::str![
"--json
--toml
--yaml"
]
);

assert_data_eq!(
complete!(cmd, "--format --json --j[TAB]"),
snapbox::str!["--json"]
);

assert_data_eq!(
complete!(cmd, "-F --json --j[TAB]"),
snapbox::str!["--json"]
);
}

#[test]
fn suggest_positional_long_allow_hyphen() {
let mut cmd = Command::new("exhaustive")
.arg(
clap::Arg::new("format")
.long("format")
.short('F')
.allow_hyphen_values(true)
.value_parser(["--json", "--toml", "--yaml"]),
)
.arg(
clap::Arg::new("positional_a")
.value_parser(["--pos_a"])
.index(1)
.allow_hyphen_values(true),
)
.arg(
clap::Arg::new("positional_b")
.index(2)
.value_parser(["pos_b"]),
);

assert_data_eq!(
complete!(cmd, "--format --json --pos[TAB]"),
snapbox::str!["--pos_a"]
);
assert_data_eq!(
complete!(cmd, "-F --json --pos[TAB]"),
snapbox::str!["--pos_a"]
);

assert_data_eq!(
complete!(cmd, "--format --json --pos_a [TAB]"),
snapbox::str![
"--format
--help Print help
-F
-h Print help
pos_b"
]
);
assert_data_eq!(
complete!(cmd, "-F --json --pos_a [TAB]"),
snapbox::str![
"--format
--help Print help
-F
-h Print help
pos_b"
]
);

assert_data_eq!(
complete!(cmd, "--format --json --pos_a p[TAB]"),
snapbox::str!["pos_b"]
);
assert_data_eq!(
complete!(cmd, "-F --json --pos_a p[TAB]"),
snapbox::str!["pos_b"]
);
}

#[test]
fn suggest_positional_short_allow_hyphen() {
let mut cmd = Command::new("exhaustive")
.arg(
clap::Arg::new("format")
.long("format")
.short('F')
.allow_hyphen_values(true)
.value_parser(["--json", "--toml", "--yaml"]),
)
.arg(
clap::Arg::new("positional_a")
.value_parser(["-a"])
.index(1)
.allow_hyphen_values(true),
)
.arg(
clap::Arg::new("positional_b")
.index(2)
.value_parser(["pos_b"]),
);

assert_data_eq!(
complete!(cmd, "--format --json -a [TAB]"),
snapbox::str![
"--format
--help Print help
-F
-h Print help
pos_b"
]
);
assert_data_eq!(
complete!(cmd, "-F --json -a [TAB]"),
snapbox::str![
"--format
--help Print help
-F
-h Print help
pos_b"
]
);

assert_data_eq!(
complete!(cmd, "--format --json -a p[TAB]"),
snapbox::str!["pos_b"]
);
assert_data_eq!(
complete!(cmd, "-F --json -a p[TAB]"),
snapbox::str!["pos_b"]
);
}

fn complete(cmd: &mut Command, args: impl AsRef<str>, current_dir: Option<&Path>) -> String {
let input = args.as_ref();
let mut args = vec![std::ffi::OsString::from(cmd.get_name())];
Expand Down

0 comments on commit 6b7aa3d

Please sign in to comment.