You've already forked uutils-args
mirror of
https://github.com/uutils/uutils-args.git
synced 2026-06-10 16:13:08 -07:00
add suggestions for long and dd-style options
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user