From 99e37cca6c8280d3293746c144ab60a1d2522705 Mon Sep 17 00:00:00 2001 From: Amin Yahyaabadi Date: Sun, 12 Nov 2023 14:25:25 -0800 Subject: [PATCH 1/3] feat: suggest fuzzy matches in case of unrecognized arguments --- argh/Cargo.toml | 1 + argh/src/lib.rs | 30 +++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/argh/Cargo.toml b/argh/Cargo.toml index 557706c..354d97d 100644 --- a/argh/Cargo.toml +++ b/argh/Cargo.toml @@ -12,6 +12,7 @@ readme = "README.md" [dependencies] argh_shared = { version = "0.1.12", path = "../argh_shared" } argh_derive = { version = "0.1.12", path = "../argh_derive" } +rust-fuzzy-search = "0.1.1" [dev-dependencies] once_cell = "1.10.0" diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 46b1fc0..a47e15a 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -332,6 +332,8 @@ pub type SubCommandInfo = argh_shared::SubCommandInfo<'static>; pub use argh_shared::{ErrorCodeInfo, FlagInfo, FlagInfoKind, Optionality, PositionalInfo}; +use rust_fuzzy_search::fuzzy_search_best_n; + /// Structured information about the command line arguments. pub trait ArgsInfo { /// Returns the argument info. @@ -976,7 +978,13 @@ impl<'a> ParseStructOptions<'a> { .arg_to_slot .iter() .find_map(|&(name, pos)| if name == arg { Some(pos) } else { None }) - .ok_or_else(|| unrecognized_argument(arg))?; + .ok_or_else(|| { + unrecognized_argument( + arg, + self.arg_to_slot, + &vec!["--help".to_owned(), "help".to_owned()], + ) + })?; match self.slots[pos] { ParseStructOption::Flag(ref mut b) => b.set_flag(arg), @@ -996,8 +1004,24 @@ impl<'a> ParseStructOptions<'a> { } } -fn unrecognized_argument(x: &str) -> String { - ["Unrecognized argument: ", x, "\n"].concat() +fn unrecognized_argument( + given: &str, + arg_to_slot: &[(&str, usize)], + extra_suggestions: &[String], +) -> String { + // get the list of available arguments + let available = arg_to_slot + .iter() + .map(|(name, _pos)| *name) + .chain(extra_suggestions.iter().map(|s| s.as_str())) + .collect::>(); + + if available.is_empty() { + return format!("Unrecognized argument: \"{}\"", given); + } + + let suggestions = fuzzy_search_best_n(given, &available, 1); + format!("Unrecognized argument: \"{}\". Did you mean \"{}\"?", given, suggestions[0].0) } // `--` or `-` options, including a mutable reference to their value. From c0e447faa7ecff06b7d7ee84cd3c73ed5f99562a Mon Sep 17 00:00:00 2001 From: Amin Yahyaabadi Date: Sun, 12 Nov 2023 14:40:11 -0800 Subject: [PATCH 2/3] fix: update the test for fuzzy suggestions --- argh/src/lib.rs | 6 +++--- argh/tests/lib.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/argh/src/lib.rs b/argh/src/lib.rs index a47e15a..7710c52 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -982,7 +982,7 @@ impl<'a> ParseStructOptions<'a> { unrecognized_argument( arg, self.arg_to_slot, - &vec!["--help".to_owned(), "help".to_owned()], + &["--help".to_owned(), "help".to_owned()], ) })?; @@ -1017,11 +1017,11 @@ fn unrecognized_argument( .collect::>(); if available.is_empty() { - return format!("Unrecognized argument: \"{}\"", given); + return format!("Unrecognized argument: \"{}\"\n", given); } let suggestions = fuzzy_search_best_n(given, &available, 1); - format!("Unrecognized argument: \"{}\". Did you mean \"{}\"?", given, suggestions[0].0) + format!("Unrecognized argument: \"{}\". Did you mean \"{}\"?\n", given, suggestions[0].0) } // `--` or `-` options, including a mutable reference to their value. diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index d6a2fef..6fed023 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -911,7 +911,7 @@ mod fuchsia_commandline_tools_rubric { let e = OneOption::from_args(&["cmdname"], &["--foo=bar"]) .expect_err("Parsing option value using `=` should fail"); - assert_eq!(e.output, "Unrecognized argument: --foo=bar\n"); + assert_eq!(e.output, "Unrecognized argument: \"--foo=bar\". Did you mean \"--foo\"?\n"); assert!(e.status.is_err()); } From 364f7a85dacf56c70f0977e6999cea4980f7aec6 Mon Sep 17 00:00:00 2001 From: Amin Yahyaabadi Date: Mon, 13 Nov 2023 14:11:19 -0800 Subject: [PATCH 3/3] fix: use the help triggers as extra suggestions --- argh/src/lib.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 7710c52..d56d1f0 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -978,13 +978,7 @@ impl<'a> ParseStructOptions<'a> { .arg_to_slot .iter() .find_map(|&(name, pos)| if name == arg { Some(pos) } else { None }) - .ok_or_else(|| { - unrecognized_argument( - arg, - self.arg_to_slot, - &["--help".to_owned(), "help".to_owned()], - ) - })?; + .ok_or_else(|| unrecognized_argument(arg, self.arg_to_slot, self.help_triggers))?; match self.slots[pos] { ParseStructOption::Flag(ref mut b) => b.set_flag(arg), @@ -1007,13 +1001,13 @@ impl<'a> ParseStructOptions<'a> { fn unrecognized_argument( given: &str, arg_to_slot: &[(&str, usize)], - extra_suggestions: &[String], + extra_suggestions: &[&str], ) -> String { // get the list of available arguments let available = arg_to_slot .iter() .map(|(name, _pos)| *name) - .chain(extra_suggestions.iter().map(|s| s.as_str())) + .chain(extra_suggestions.iter().copied()) .collect::>(); if available.is_empty() {