Skip to content

Commit 6b7aa3d

Browse files
authored
Merge pull request #5628 from mart-mihkel/complete_hyphen
Support `allow_hyphen_values` in native completions
2 parents 64e3790 + 57b6cb8 commit 6b7aa3d

File tree

2 files changed

+206
-11
lines changed

2 files changed

+206
-11
lines changed

clap_complete/src/engine/complete.rs

+45-11
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ pub fn complete(
5858
parse_positional(current_cmd, pos_index, is_escaped, current_state);
5959
} else if arg.is_escape() {
6060
is_escaped = true;
61+
} else if opt_allows_hyphen(&current_state, &arg) {
62+
match current_state {
63+
ParseState::Opt((opt, count)) => next_state = parse_opt_value(opt, count),
64+
_ => unreachable!("else branch is only reachable in Opt state"),
65+
}
6166
} else if let Some((flag, value)) = arg.to_long() {
6267
if let Ok(flag) = flag {
6368
let opt = current_cmd.get_arguments().find(|a| {
@@ -69,10 +74,14 @@ pub fn complete(
6974
});
7075
is_find.unwrap_or(false)
7176
});
72-
if opt.map(|o| o.get_action().takes_values()).unwrap_or(false) {
73-
if value.is_none() {
74-
next_state = ParseState::Opt((opt.unwrap(), 1));
77+
78+
if let Some(opt) = opt {
79+
if opt.get_action().takes_values() && value.is_none() {
80+
next_state = ParseState::Opt((opt, 1));
7581
};
82+
} else if pos_allows_hyphen(current_cmd, pos_index) {
83+
(next_state, pos_index) =
84+
parse_positional(current_cmd, pos_index, is_escaped, current_state);
7685
}
7786
}
7887
} else if let Some(short) = arg.to_short() {
@@ -81,21 +90,17 @@ pub fn complete(
8190
if short.next_value_os().is_none() {
8291
next_state = ParseState::Opt((opt, 1));
8392
}
93+
} else if pos_allows_hyphen(current_cmd, pos_index) {
94+
(next_state, pos_index) =
95+
parse_positional(current_cmd, pos_index, is_escaped, current_state);
8496
}
8597
} else {
8698
match current_state {
8799
ParseState::ValueDone | ParseState::Pos(..) => {
88100
(next_state, pos_index) =
89101
parse_positional(current_cmd, pos_index, is_escaped, current_state);
90102
}
91-
92-
ParseState::Opt((opt, count)) => {
93-
let range = opt.get_num_args().expect("built");
94-
let max = range.max_values();
95-
if count < max {
96-
next_state = ParseState::Opt((opt, count + 1));
97-
}
98-
}
103+
ParseState::Opt((opt, count)) => next_state = parse_opt_value(opt, count),
99104
}
100105
}
101106
}
@@ -546,3 +551,32 @@ fn parse_positional<'a>(
546551
),
547552
}
548553
}
554+
555+
/// Parse optional flag argument. Return new state
556+
fn parse_opt_value(opt: &clap::Arg, count: usize) -> ParseState<'_> {
557+
let range = opt.get_num_args().expect("built");
558+
let max = range.max_values();
559+
if count < max {
560+
ParseState::Opt((opt, count + 1))
561+
} else {
562+
ParseState::ValueDone
563+
}
564+
}
565+
566+
fn pos_allows_hyphen(cmd: &clap::Command, pos_index: usize) -> bool {
567+
cmd.get_positionals()
568+
.find(|a| a.get_index() == Some(pos_index))
569+
.map(|p| p.is_allow_hyphen_values_set())
570+
.unwrap_or(false)
571+
}
572+
573+
fn opt_allows_hyphen(state: &ParseState<'_>, arg: &clap_lex::ParsedArg<'_>) -> bool {
574+
let val = arg.to_value_os();
575+
if val.starts_with("-") {
576+
if let ParseState::Opt((opt, _)) = state {
577+
return opt.is_allow_hyphen_values_set();
578+
}
579+
}
580+
581+
false
582+
}

clap_complete/tests/testsuite/engine.rs

+161
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,167 @@ a_pos,c_pos"
981981
);
982982
}
983983

984+
#[test]
985+
fn suggest_allow_hyphen() {
986+
let mut cmd = Command::new("exhaustive")
987+
.arg(
988+
clap::Arg::new("format")
989+
.long("format")
990+
.short('F')
991+
.allow_hyphen_values(true)
992+
.value_parser(["--json", "--toml", "--yaml"]),
993+
)
994+
.arg(clap::Arg::new("json").long("json"));
995+
996+
assert_data_eq!(complete!(cmd, "--format --j[TAB]"), snapbox::str!["--json"]);
997+
assert_data_eq!(complete!(cmd, "-F --j[TAB]"), snapbox::str!["--json"]);
998+
assert_data_eq!(complete!(cmd, "--format --t[TAB]"), snapbox::str!["--toml"]);
999+
assert_data_eq!(complete!(cmd, "-F --t[TAB]"), snapbox::str!["--toml"]);
1000+
1001+
assert_data_eq!(
1002+
complete!(cmd, "--format --[TAB]"),
1003+
snapbox::str![
1004+
"--json
1005+
--toml
1006+
--yaml"
1007+
]
1008+
);
1009+
1010+
assert_data_eq!(
1011+
complete!(cmd, "-F --[TAB]"),
1012+
snapbox::str![
1013+
"--json
1014+
--toml
1015+
--yaml"
1016+
]
1017+
);
1018+
1019+
assert_data_eq!(
1020+
complete!(cmd, "--format --json --j[TAB]"),
1021+
snapbox::str!["--json"]
1022+
);
1023+
1024+
assert_data_eq!(
1025+
complete!(cmd, "-F --json --j[TAB]"),
1026+
snapbox::str!["--json"]
1027+
);
1028+
}
1029+
1030+
#[test]
1031+
fn suggest_positional_long_allow_hyphen() {
1032+
let mut cmd = Command::new("exhaustive")
1033+
.arg(
1034+
clap::Arg::new("format")
1035+
.long("format")
1036+
.short('F')
1037+
.allow_hyphen_values(true)
1038+
.value_parser(["--json", "--toml", "--yaml"]),
1039+
)
1040+
.arg(
1041+
clap::Arg::new("positional_a")
1042+
.value_parser(["--pos_a"])
1043+
.index(1)
1044+
.allow_hyphen_values(true),
1045+
)
1046+
.arg(
1047+
clap::Arg::new("positional_b")
1048+
.index(2)
1049+
.value_parser(["pos_b"]),
1050+
);
1051+
1052+
assert_data_eq!(
1053+
complete!(cmd, "--format --json --pos[TAB]"),
1054+
snapbox::str!["--pos_a"]
1055+
);
1056+
assert_data_eq!(
1057+
complete!(cmd, "-F --json --pos[TAB]"),
1058+
snapbox::str!["--pos_a"]
1059+
);
1060+
1061+
assert_data_eq!(
1062+
complete!(cmd, "--format --json --pos_a [TAB]"),
1063+
snapbox::str![
1064+
"--format
1065+
--help Print help
1066+
-F
1067+
-h Print help
1068+
pos_b"
1069+
]
1070+
);
1071+
assert_data_eq!(
1072+
complete!(cmd, "-F --json --pos_a [TAB]"),
1073+
snapbox::str![
1074+
"--format
1075+
--help Print help
1076+
-F
1077+
-h Print help
1078+
pos_b"
1079+
]
1080+
);
1081+
1082+
assert_data_eq!(
1083+
complete!(cmd, "--format --json --pos_a p[TAB]"),
1084+
snapbox::str!["pos_b"]
1085+
);
1086+
assert_data_eq!(
1087+
complete!(cmd, "-F --json --pos_a p[TAB]"),
1088+
snapbox::str!["pos_b"]
1089+
);
1090+
}
1091+
1092+
#[test]
1093+
fn suggest_positional_short_allow_hyphen() {
1094+
let mut cmd = Command::new("exhaustive")
1095+
.arg(
1096+
clap::Arg::new("format")
1097+
.long("format")
1098+
.short('F')
1099+
.allow_hyphen_values(true)
1100+
.value_parser(["--json", "--toml", "--yaml"]),
1101+
)
1102+
.arg(
1103+
clap::Arg::new("positional_a")
1104+
.value_parser(["-a"])
1105+
.index(1)
1106+
.allow_hyphen_values(true),
1107+
)
1108+
.arg(
1109+
clap::Arg::new("positional_b")
1110+
.index(2)
1111+
.value_parser(["pos_b"]),
1112+
);
1113+
1114+
assert_data_eq!(
1115+
complete!(cmd, "--format --json -a [TAB]"),
1116+
snapbox::str![
1117+
"--format
1118+
--help Print help
1119+
-F
1120+
-h Print help
1121+
pos_b"
1122+
]
1123+
);
1124+
assert_data_eq!(
1125+
complete!(cmd, "-F --json -a [TAB]"),
1126+
snapbox::str![
1127+
"--format
1128+
--help Print help
1129+
-F
1130+
-h Print help
1131+
pos_b"
1132+
]
1133+
);
1134+
1135+
assert_data_eq!(
1136+
complete!(cmd, "--format --json -a p[TAB]"),
1137+
snapbox::str!["pos_b"]
1138+
);
1139+
assert_data_eq!(
1140+
complete!(cmd, "-F --json -a p[TAB]"),
1141+
snapbox::str!["pos_b"]
1142+
);
1143+
}
1144+
9841145
fn complete(cmd: &mut Command, args: impl AsRef<str>, current_dir: Option<&Path>) -> String {
9851146
let input = args.as_ref();
9861147
let mut args = vec![std::ffi::OsString::from(cmd.get_name())];

0 commit comments

Comments
 (0)