1#[cfg(feature = "suggestions")]
2use std::cmp::Ordering;
3
4// Internal
5use crate::builder::Command;
6
7/// Find strings from an iterable of `possible_values` similar to a given value `v`
8/// Returns a Vec of all possible values that exceed a similarity threshold
9/// sorted by ascending similarity, most similar comes last
10#[cfg(feature = "suggestions")]
11pub(crate) fn did_you_mean<T, I>(v: &str, possible_values: I) -> Vec<String>
12where
13 T: AsRef<str>,
14 I: IntoIterator<Item = T>,
15{
16 let mut candidates: Vec<(f64, String)> = possible_values
17 .into_iter()
18 // GH #4660: using `jaro` because `jaro_winkler` implementation in `strsim-rs` is wrong
19 // causing strings with common prefix >=10 to be considered perfectly similar
20 .map(|pv| (strsim::jaro(v, pv.as_ref()), pv.as_ref().to_owned()))
21 // Confidence of 0.7 so that bar -> baz is suggested
22 .filter(|(confidence, _)| *confidence > 0.7)
23 .collect();
24 candidates.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal));
25 candidates.into_iter().map(|(_, pv)| pv).collect()
26}
27
28#[cfg(not(feature = "suggestions"))]
29pub(crate) fn did_you_mean<T, I>(_: &str, _: I) -> Vec<String>
30where
31 T: AsRef<str>,
32 I: IntoIterator<Item = T>,
33{
34 Vec::new()
35}
36
37/// Returns a suffix that can be empty, or is the standard 'did you mean' phrase
38pub(crate) fn did_you_mean_flag<'a, 'help, I, T>(
39 arg: &str,
40 remaining_args: &[&std::ffi::OsStr],
41 longs: I,
42 subcommands: impl IntoIterator<Item = &'a mut Command>,
43) -> Option<(String, Option<String>)>
44where
45 'help: 'a,
46 T: AsRef<str>,
47 I: IntoIterator<Item = T>,
48{
49 use crate::mkeymap::KeyType;
50
51 match did_you_mean(arg, longs).pop() {
52 Some(candidate) => Some((candidate, None)),
53 None => subcommands
54 .into_iter()
55 .filter_map(|subcommand| {
56 subcommand._build_self(false);
57
58 let longs = subcommand.get_keymap().keys().filter_map(|a| {
59 if let KeyType::Long(v) = a {
60 Some(v.to_string_lossy().into_owned())
61 } else {
62 None
63 }
64 });
65
66 let subcommand_name = subcommand.get_name();
67
68 let candidate = some!(did_you_mean(arg, longs).pop());
69 let score = some!(remaining_args.iter().position(|x| subcommand_name == *x));
70 Some((score, (candidate, Some(subcommand_name.to_string()))))
71 })
72 .min_by_key(|(x, _)| *x)
73 .map(|(_, suggestion)| suggestion),
74 }
75}
76
77#[cfg(all(test, feature = "suggestions"))]
78mod test {
79 use super::*;
80
81 #[test]
82 fn missing_letter() {
83 let p_vals = ["test", "possible", "values"];
84 assert_eq!(did_you_mean("tst", p_vals.iter()), vec!["test"]);
85 }
86
87 #[test]
88 fn ambiguous() {
89 let p_vals = ["test", "temp", "possible", "values"];
90 assert_eq!(did_you_mean("te", p_vals.iter()), vec!["test", "temp"]);
91 }
92
93 #[test]
94 fn unrelated() {
95 let p_vals = ["test", "possible", "values"];
96 assert_eq!(
97 did_you_mean("hahaahahah", p_vals.iter()),
98 Vec::<String>::new()
99 );
100 }
101
102 #[test]
103 fn best_fit() {
104 let p_vals = [
105 "test",
106 "possible",
107 "values",
108 "alignmentStart",
109 "alignmentScore",
110 ];
111 assert_eq!(
112 did_you_mean("alignmentScorr", p_vals.iter()),
113 vec!["alignmentStart", "alignmentScore"]
114 );
115 }
116
117 #[test]
118 fn best_fit_long_common_prefix_issue_4660() {
119 let p_vals = ["alignmentScore", "alignmentStart"];
120 assert_eq!(
121 did_you_mean("alignmentScorr", p_vals.iter()),
122 vec!["alignmentStart", "alignmentScore"]
123 );
124 }
125
126 #[test]
127 fn flag_missing_letter() {
128 let p_vals = ["test", "possible", "values"];
129 assert_eq!(
130 did_you_mean_flag("tst", &[], p_vals.iter(), []),
131 Some(("test".to_owned(), None))
132 );
133 }
134
135 #[test]
136 fn flag_ambiguous() {
137 let p_vals = ["test", "temp", "possible", "values"];
138 assert_eq!(
139 did_you_mean_flag("te", &[], p_vals.iter(), []),
140 Some(("temp".to_owned(), None))
141 );
142 }
143
144 #[test]
145 fn flag_unrelated() {
146 let p_vals = ["test", "possible", "values"];
147 assert_eq!(
148 did_you_mean_flag("hahaahahah", &[], p_vals.iter(), []),
149 None
150 );
151 }
152
153 #[test]
154 fn flag_best_fit() {
155 let p_vals = [
156 "test",
157 "possible",
158 "values",
159 "alignmentStart",
160 "alignmentScore",
161 ];
162 assert_eq!(
163 did_you_mean_flag("alignmentScorr", &[], p_vals.iter(), []),
164 Some(("alignmentScore".to_owned(), None))
165 );
166 }
167}
168