diff --git a/Cargo.toml b/Cargo.toml index 4a5b26f..3f5b885 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ readme = "README.md" [dependencies] derive = { version = "0.1.0", path = "derive" } +strsim = "0.10.0" lexopt = "0.3.0" [workspace] diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 46c3289..3e14da7 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -185,7 +185,7 @@ pub(crate) fn short_handling(args: &[Argument]) -> (TokenStream, Vec) { Ok(Some(Argument::Custom( match short { #(#match_arms)* - _ => return Err(Error::UnexpectedOption(short.to_string())), + _ => return Err(::uutils_args::Error::UnexpectedOption(short.to_string(), Vec::new())), } ))) ); @@ -231,14 +231,19 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea } if options.is_empty() { - return quote!(return Err(Error::UnexpectedOption(long.to_string()))); + return quote!( + return Err(::uutils_args::Error::UnexpectedOption( + long.to_string(), + Vec::new() + )) + ); } // TODO: Add version check let help_check = if !help_flags.long.is_empty() { let long_help_flags = help_flags.long.iter().map(|f| &f.flag); quote!(if let #(#long_help_flags)|* = long { - return Ok(Some(Argument::Help)); + return Ok(Some(::uutils_args::Argument::Help)); }) } else { quote!() @@ -248,26 +253,7 @@ pub(crate) fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStrea quote!( let long_options: [&str; #num_opts] = [#(#options),*]; - let mut candidates = Vec::new(); - let mut exact_match = None; - for opt in long_options { - if opt == long { - exact_match = Some(opt); - break; - } else if opt.starts_with(long) { - candidates.push(opt); - } - } - - let long = match (exact_match, &candidates[..]) { - (Some(opt), _) => opt, - (None, [opt]) => opt, - (None, []) => return Err(Error::UnexpectedOption(long.to_string())), - (None, opts) => return Err(Error::AmbiguousOption { - option: long.to_string(), - candidates: candidates.iter().map(|s| s.to_string()).collect(), - }) - }; + let long = ::uutils_args::infer_long_option(long, &long_options)?; #help_check @@ -307,6 +293,7 @@ pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { // dd-style arguments let mut dd_branches = Vec::new(); + let mut dd_args = Vec::new(); for arg @ Argument { arg_type, .. } in args { let flags = match arg_type { ArgType::Option { flags, .. } => flags, @@ -317,6 +304,7 @@ pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { for (prefix, _) in &flags.dd_style { let ident = &arg.ident; + dd_args.push(prefix); dd_branches.push(quote!( if prefix == #prefix { let value = ::uutils_args::parse_value_for_option("", ::std::ffi::OsStr::new(value))?; @@ -331,15 +319,19 @@ pub(crate) fn free_handling(args: &[Argument]) -> TokenStream { if_expressions.push(quote!( if let Some((prefix, value)) = arg.split_once('=') { #(#dd_branches)* + + return Err(::uutils_args::Error::UnexpectedOption(prefix.to_string(), ::uutils_args::filter_suggestions(prefix, &[#(#dd_args),*], ""))); } )); } - quote!(if let Some(mut raw) = parser.try_raw_args() { - if let Some(arg) = raw.peek().and_then(|s| s.to_str()) { - #(#if_expressions)* + quote!( + if let Some(mut raw) = parser.try_raw_args() { + if let Some(arg) = raw.peek().and_then(|s| s.to_str()) { + #(#if_expressions)* + } } - }) + ) } pub(crate) fn positional_handling(args: &[Argument]) -> (TokenStream, TokenStream) { diff --git a/src/error.rs b/src/error.rs index 67593fc..9c6c2a0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,7 +15,9 @@ pub enum Error { MissingPositionalArguments(Vec), /// An unrecognized option was passed. - UnexpectedOption(String), + /// + /// The second argument is a list of suggestions + UnexpectedOption(String, Vec), /// No more positional arguments were expected, but one was given anyway. UnexpectedArgument(OsString), @@ -75,8 +77,12 @@ impl Display for Error { } Ok(()) } - Error::UnexpectedOption(opt) => { - write!(f, "Found an invalid option '{opt}'.") + Error::UnexpectedOption(opt, suggestions) => { + write!(f, "Found an invalid option '{opt}'.")?; + if !suggestions.is_empty() { + write!(f, "\nDid you mean: {}", suggestions.join(", "))?; + } + Ok(()) } Error::UnexpectedArgument(arg) => { write!(f, "Found an invalid argument '{}'.", arg.to_string_lossy()) @@ -123,7 +129,7 @@ impl From for Error { fn from(other: lexopt::Error) -> Error { match other { lexopt::Error::MissingValue { option } => Self::MissingValue { option }, - lexopt::Error::UnexpectedOption(s) => Self::UnexpectedOption(s), + lexopt::Error::UnexpectedOption(s) => Self::UnexpectedOption(s, Vec::new()), lexopt::Error::UnexpectedArgument(s) => Self::UnexpectedArgument(s), lexopt::Error::UnexpectedValue { option, value } => { Self::UnexpectedValue { option, value } diff --git a/src/lib.rs b/src/lib.rs index 15fa52a..80a95df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -413,6 +413,44 @@ pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result( + input: &'a str, + long_options: &'a [&'a str], +) -> Result<&'a str, Error> { + let mut candidates = Vec::new(); + let mut exact_match = None; + for opt in long_options { + if *opt == input { + exact_match = Some(opt); + break; + } else if opt.starts_with(input) { + candidates.push(opt); + } + } + + match (exact_match, &candidates[..]) { + (Some(opt), _) => Ok(*opt), + (None, [opt]) => Ok(**opt), + (None, []) => Err(Error::UnexpectedOption( + format!("--{input}"), + filter_suggestions(input, long_options, "--"), + )), + (None, _) => Err(Error::AmbiguousOption { + option: input.to_string(), + candidates: candidates.iter().map(|s| s.to_string()).collect(), + }), + } +} + +/// Filter a list of options to just the elements that are similar to the given string +pub fn filter_suggestions(input: &str, long_options: &[&str], prefix: &str) -> Vec { + long_options + .iter() + .filter(|opt| strsim::jaro(input, opt) > 0.7) + .map(|o| format!("{prefix}{o}")) + .collect() +} + #[cfg(test)] mod test { use std::ffi::OsStr;