Skip to content

Commit

Permalink
Merge pull request #73 from tertsdiepraam/internal-module
Browse files Browse the repository at this point in the history
Move helper functions to `internal` module
  • Loading branch information
cakebaker authored Dec 14, 2023
2 parents abd987e + 787d685 commit f24edac
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 163 deletions.
19 changes: 11 additions & 8 deletions derive/src/argument.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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))));
}
Expand All @@ -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))));
}
Expand All @@ -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),*], "")
));
}
));
}
Expand Down Expand Up @@ -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)?)
)
}

Expand All @@ -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::<Result<_,_>>()?;
Self::#ident(collection)
})
Expand Down
2 changes: 1 addition & 1 deletion derive/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!()
};
Expand Down
2 changes: 1 addition & 1 deletion derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()?
Expand Down
161 changes: 161 additions & 0 deletions src/internal.rs
Original file line number Diff line number Diff line change
@@ -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<OsString> {
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<T: Value>(parser: &mut lexopt::Parser, prefix: &'static str) -> Option<T> {
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<T: Value>(opt: &str, v: &OsStr) -> Result<T, Error> {
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<String> {
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<Item = (&'static str, &'static str)>,
) -> 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']));
}
}
Loading

0 comments on commit f24edac

Please sign in to comment.