add suggestions for long and dd-style options

This commit is contained in:
Terts Diepraam
2023-11-09 14:10:32 +01:00
parent 87fc2f3d7c
commit baa088baf1
4 changed files with 68 additions and 31 deletions
+1
View File
@@ -11,6 +11,7 @@ readme = "README.md"
[dependencies]
derive = { version = "0.1.0", path = "derive" }
strsim = "0.10.0"
lexopt = "0.3.0"
[workspace]
+19 -27
View File
@@ -185,7 +185,7 @@ pub(crate) fn short_handling(args: &[Argument]) -> (TokenStream, Vec<char>) {
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) {
+10 -4
View File
@@ -15,7 +15,9 @@ pub enum Error {
MissingPositionalArguments(Vec<String>),
/// An unrecognized option was passed.
UnexpectedOption(String),
///
/// The second argument is a list of suggestions
UnexpectedOption(String, Vec<String>),
/// 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<lexopt::Error> 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 }
+38
View File
@@ -413,6 +413,44 @@ pub fn parse_value_for_option<T: Value>(opt: &str, v: &OsStr) -> Result<T, Error
})
}
pub fn infer_long_option<'a>(
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<String> {
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;