clap_builder/parser/features/
suggestions.rs

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