diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 7d194f1..a4d9e77 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -254,7 +254,7 @@ pub fn long_handling(args: &[Argument], help_flags: &Flags) -> TokenStream { quote!( let long_options: [&str; #num_opts] = [#(#options),*]; - let long = ::uutils_args::infer_long_option(long, &long_options)?; + let long = ::uutils_args::internal::infer_long_option(long, &long_options)?; #help_check @@ -284,7 +284,7 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { if_expressions.push(quote!( if let Some(inner) = #filter(arg) { - let value = ::uutils_args::parse_value_for_option("", ::std::ffi::OsStr::new(inner))?; + let value = ::uutils_args::internal::parse_value_for_option("", ::std::ffi::OsStr::new(inner))?; let _ = raw.next(); return Ok(Some(Argument::Custom(Self::#ident(value)))); } @@ -308,7 +308,7 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { dd_args.push(prefix); dd_branches.push(quote!( if prefix == #prefix { - let value = ::uutils_args::parse_value_for_option("", ::std::ffi::OsStr::new(value))?; + let value = ::uutils_args::internal::parse_value_for_option("", ::std::ffi::OsStr::new(value))?; let _ = raw.next(); return Ok(Some(Argument::Custom(Self::#ident(value)))); } @@ -321,7 +321,10 @@ pub fn free_handling(args: &[Argument]) -> TokenStream { 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),*], ""))); + return Err(::uutils_args::Error::UnexpectedOption( + prefix.to_string(), + ::uutils_args::internal::filter_suggestions(prefix, &[#(#dd_args),*], "") + )); } )); } @@ -410,19 +413,19 @@ fn default_value_expression(ident: &Ident, default_expr: &TokenStream) -> TokenS fn optional_value_expression(ident: &Ident, default_expr: &TokenStream) -> TokenStream { quote!(match parser.optional_value() { - Some(value) => Self::#ident(::uutils_args::parse_value_for_option(&option, &value)?), + Some(value) => Self::#ident(::uutils_args::internal::parse_value_for_option(&option, &value)?), None => Self::#ident(#default_expr), }) } fn required_value_expression(ident: &Ident) -> TokenStream { - quote!(Self::#ident(::uutils_args::parse_value_for_option(&option, &parser.value()?)?)) + quote!(Self::#ident(::uutils_args::internal::parse_value_for_option(&option, &parser.value()?)?)) } fn positional_expression(ident: &Ident) -> TokenStream { // TODO: Add option name in this from_value call quote!( - Self::#ident(::uutils_args::parse_value_for_option("", &value)?) + Self::#ident(::uutils_args::internal::parse_value_for_option("", &value)?) ) } @@ -432,7 +435,7 @@ fn last_positional_expression(ident: &Ident) -> TokenStream { let raw_args = parser.raw_args()?; let collection = std::iter::once(value) .chain(raw_args) - .map(|v| ::uutils_args::parse_value_for_option("", &v)) + .map(|v| ::uutils_args::internal::parse_value_for_option("", &v)) .collect::>()?; Self::#ident(collection) }) diff --git a/derive/src/help.rs b/derive/src/help.rs index 9ebeaf2..ad5d5ae 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -75,7 +75,7 @@ pub fn help_string( } let options = if !options.is_empty() { - quote!(::uutils_args::print_flags(&mut w, #indent, #width, [#(#options),*])?;) + quote!(::uutils_args::internal::print_flags(&mut w, #indent, #width, [#(#options),*])?;) } else { quote!() }; diff --git a/derive/src/lib.rs b/derive/src/lib.rs index cd8ccbf..5967c09 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -57,7 +57,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { // This is a bit of a hack to support `echo` and should probably not be // used in general. let next_arg = if arguments_attr.parse_echo_style { - quote!(if let Some(val) = uutils_args::__echo_style_positional(parser, &[#(#short_flags),*]) { + quote!(if let Some(val) = ::uutils_args::internal::echo_style_positional(parser, &[#(#short_flags),*]) { Some(lexopt::Arg::Value(val)) } else { parser.next()? diff --git a/src/internal.rs b/src/internal.rs new file mode 100644 index 0000000..8a13d57 --- /dev/null +++ b/src/internal.rs @@ -0,0 +1,161 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Functions to be used by `uutils-args-derive`. +//! +//! This has the following implications: +//! - These functions are not guaranteed to be stable. +//! - These functions should not be used outside the derive crate +//! +//! Yet, they should be properly documented to make macro-expanded code +//! readable. + +use super::{Error, Value}; +use std::{ + ffi::{OsStr, OsString}, + io::Write, +}; + +/// Parses an echo-style positional argument +/// +/// This means that any argument that does not solely consist of a hyphen +/// followed by the characters in the list of `short_args` is considered +/// to be a positional argument, instead of an invalid argument. This +/// includes the `--` argument, which is ignored by `echo`. +pub fn echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> Option { + let mut raw = p.try_raw_args()?; + let val = raw.peek()?; + + if is_echo_style_positional(val, short_args) { + let val = val.into(); + raw.next(); + Some(val) + } else { + None + } +} + +fn is_echo_style_positional(s: &OsStr, short_args: &[char]) -> bool { + let s = match s.to_str() { + Some(x) => x, + // If it's invalid utf-8 then it can't be a short arg, so must + // be a positional argument. + None => return true, + }; + let mut chars = s.chars(); + let is_short_args = chars.next() == Some('-') && chars.all(|c| short_args.contains(&c)); + !is_short_args +} + +/// Parse an argument defined by a prefix +pub fn parse_prefix(parser: &mut lexopt::Parser, prefix: &'static str) -> Option { + let mut raw = parser.try_raw_args()?; + + // TODO: The to_str call is a limitation. Maybe we need to pull in something like bstr + let arg = raw.peek()?.to_str()?; + let value_str = arg.strip_prefix(prefix)?; + + let value = T::from_value(OsStr::new(value_str)).ok()?; + + // Consume the argument we just parsed + let _ = raw.next(); + + Some(value) +} + +/// Parse a value and wrap the error into an `Error::ParsingFailed` +pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result { + T::from_value(v).map_err(|e| Error::ParsingFailed { + option: opt.into(), + value: v.to_string_lossy().to_string(), + error: e, + }) +} + +/// Expand unambiguous prefixes to a list of candidates +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 { + long_options + .iter() + .filter(|opt| strsim::jaro(input, opt) > 0.7) + .map(|o| format!("{prefix}{o}")) + .collect() +} + +/// Print a formatted list of options. +pub fn print_flags( + mut w: impl Write, + indent_size: usize, + width: usize, + options: impl IntoIterator, +) -> std::io::Result<()> { + let indent = " ".repeat(indent_size); + writeln!(w, "\nOptions:")?; + for (flags, help_string) in options { + let mut help_lines = help_string.lines(); + write!(w, "{}{}", &indent, &flags)?; + + if flags.len() <= width { + let line = match help_lines.next() { + Some(line) => line, + None => { + writeln!(w)?; + continue; + } + }; + let help_indent = " ".repeat(width - flags.len() + 2); + writeln!(w, "{}{}", help_indent, line)?; + } else { + writeln!(w)?; + } + + let help_indent = " ".repeat(width + indent_size + 2); + for line in help_lines { + writeln!(w, "{}{}", help_indent, line)?; + } + } + Ok(()) +} + +#[cfg(test)] +mod test { + use std::ffi::OsStr; + + use super::is_echo_style_positional; + + #[test] + fn echo_positional() { + assert!(is_echo_style_positional(OsStr::new("-aaa"), &['b'])); + assert!(is_echo_style_positional(OsStr::new("--"), &['b'])); + assert!(!is_echo_style_positional(OsStr::new("-b"), &['b'])); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5ec42d2..d259d9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,6 +4,7 @@ #![doc = include_str!("../README.md")] mod error; +pub mod internal; mod value; pub use lexopt; @@ -12,11 +13,7 @@ pub use uutils_args_derive::*; pub use error::Error; pub use value::{Value, ValueError, ValueResult}; -use std::{ - ffi::{OsStr, OsString}, - io::Write, - marker::PhantomData, -}; +use std::{ffi::OsString, marker::PhantomData}; /// A wrapper around a type implementing [`Arguments`] that adds `Help` /// and `Version` variants. @@ -225,29 +222,6 @@ pub trait Options: Sized { } } -/// Parses an echo-style positional argument -/// -/// This means that any argument that does not solely consist of a hyphen -/// followed by the characters in the list of `short_args` is considered -/// to be a positional argument, instead of an invalid argument. This -/// includes the `--` argument, which is ignored by `echo`. -/// -/// This function is hidden and prefixed with `__` because it should only -/// be called via the derive macros. -#[doc(hidden)] -pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> Option { - let mut raw = p.try_raw_args()?; - let val = raw.peek()?; - - if is_echo_style_positional(val, short_args) { - let val = val.into(); - raw.next(); - Some(val) - } else { - None - } -} - #[cfg(feature = "parse-is-complete")] fn print_complete, Arg: Arguments>(mut args: I) where @@ -263,128 +237,3 @@ where assert!(args.next().is_none(), "completion only takes one argument"); println!("{}", O::complete(&shell)); } - -fn is_echo_style_positional(s: &OsStr, short_args: &[char]) -> bool { - let s = match s.to_str() { - Some(x) => x, - // If it's invalid utf-8 then it can't be a short arg, so must - // be a positional argument. - None => return true, - }; - let mut chars = s.chars(); - let is_short_args = chars.next() == Some('-') && chars.all(|c| short_args.contains(&c)); - !is_short_args -} - -/// Parse an argument defined by a prefix -#[doc(hidden)] -pub fn parse_prefix(parser: &mut lexopt::Parser, prefix: &'static str) -> Option { - let mut raw = parser.try_raw_args()?; - - // TODO: The to_str call is a limitation. Maybe we need to pull in something like bstr - let arg = raw.peek()?.to_str()?; - let value_str = arg.strip_prefix(prefix)?; - - let value = T::from_value(OsStr::new(value_str)).ok()?; - - // Consume the argument we just parsed - let _ = raw.next(); - - Some(value) -} - -/// Parse a value and wrap the error into an `Error::ParsingFailed` -#[doc(hidden)] -pub fn parse_value_for_option(opt: &str, v: &OsStr) -> Result { - T::from_value(v).map_err(|e| Error::ParsingFailed { - option: opt.into(), - value: v.to_string_lossy().to_string(), - error: e, - }) -} - -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 { - long_options - .iter() - .filter(|opt| strsim::jaro(input, opt) > 0.7) - .map(|o| format!("{prefix}{o}")) - .collect() -} - -pub fn print_flags( - mut w: impl Write, - indent_size: usize, - width: usize, - options: impl IntoIterator, -) -> std::io::Result<()> { - let indent = " ".repeat(indent_size); - writeln!(w, "\nOptions:")?; - for (flags, help_string) in options { - let mut help_lines = help_string.lines(); - write!(w, "{}{}", &indent, &flags)?; - - if flags.len() <= width { - let line = match help_lines.next() { - Some(line) => line, - None => { - writeln!(w)?; - continue; - } - }; - let help_indent = " ".repeat(width - flags.len() + 2); - writeln!(w, "{}{}", help_indent, line)?; - } else { - writeln!(w)?; - } - - let help_indent = " ".repeat(width + indent_size + 2); - for line in help_lines { - writeln!(w, "{}{}", help_indent, line)?; - } - } - Ok(()) -} - -#[cfg(test)] -mod test { - use std::ffi::OsStr; - - use crate::is_echo_style_positional; - - #[test] - fn echo_positional() { - assert!(is_echo_style_positional(OsStr::new("-aaa"), &['b'])); - assert!(is_echo_style_positional(OsStr::new("--"), &['b'])); - assert!(!is_echo_style_positional(OsStr::new("-b"), &['b'])); - } -}