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