clap_builder/parser/features/
suggestions.rs

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}