diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 7bd64839c..adfb74128 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -26,14 +26,13 @@ use uucore::{format_usage, help_about, help_usage, show}; #[cfg(windows)] use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime}; +use uucore::shortcut_value_parser::ShortcutValueParser; + // Options const DATE: &str = "date"; const HOURS: &str = "hours"; const MINUTES: &str = "minutes"; const SECONDS: &str = "seconds"; -const HOUR: &str = "hour"; -const MINUTE: &str = "minute"; -const SECOND: &str = "second"; const NS: &str = "ns"; const ABOUT: &str = help_about!("date.md"); @@ -110,9 +109,9 @@ enum Iso8601Format { impl<'a> From<&'a str> for Iso8601Format { fn from(s: &str) -> Self { match s { - HOURS | HOUR => Self::Hours, - MINUTES | MINUTE => Self::Minutes, - SECONDS | SECOND => Self::Seconds, + HOURS => Self::Hours, + MINUTES => Self::Minutes, + SECONDS => Self::Seconds, NS => Self::Ns, DATE => Self::Date, // Note: This is caught by clap via `possible_values` @@ -131,7 +130,7 @@ impl<'a> From<&'a str> for Rfc3339Format { fn from(s: &str) -> Self { match s { DATE => Self::Date, - SECONDS | SECOND => Self::Seconds, + SECONDS => Self::Seconds, NS => Self::Ns, // Should be caught by clap _ => panic!("Invalid format: {s}"), @@ -317,7 +316,9 @@ pub fn uu_app() -> Command { .short('I') .long(OPT_ISO_8601) .value_name("FMT") - .value_parser([DATE, HOUR, HOURS, MINUTE, MINUTES, SECOND, SECONDS, NS]) + .value_parser(ShortcutValueParser::new([ + DATE, HOURS, MINUTES, SECONDS, NS, + ])) .num_args(0..=1) .default_missing_value(OPT_DATE) .help(ISO_8601_HELP_STRING), @@ -333,7 +334,7 @@ pub fn uu_app() -> Command { Arg::new(OPT_RFC_3339) .long(OPT_RFC_3339) .value_name("FMT") - .value_parser([DATE, SECOND, SECONDS, NS]) + .value_parser(ShortcutValueParser::new([DATE, SECONDS, NS])) .help(RFC_3339_HELP_STRING), ) .arg( diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index e76e540c8..ca9a48d25 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -33,6 +33,7 @@ pub use crate::mods::version_cmp; pub use crate::parser::parse_glob; pub use crate::parser::parse_size; pub use crate::parser::parse_time; +pub use crate::parser::shortcut_value_parser; // * feature-gated modules #[cfg(feature = "encoding")] diff --git a/src/uucore/src/lib/parser.rs b/src/uucore/src/lib/parser.rs index 8eae16bbf..fc3e46b5c 100644 --- a/src/uucore/src/lib/parser.rs +++ b/src/uucore/src/lib/parser.rs @@ -1,3 +1,4 @@ pub mod parse_glob; pub mod parse_size; pub mod parse_time; +pub mod shortcut_value_parser; diff --git a/src/uucore/src/lib/parser/shortcut_value_parser.rs b/src/uucore/src/lib/parser/shortcut_value_parser.rs new file mode 100644 index 000000000..0b0716158 --- /dev/null +++ b/src/uucore/src/lib/parser/shortcut_value_parser.rs @@ -0,0 +1,141 @@ +use clap::{ + builder::{PossibleValue, TypedValueParser}, + error::{ContextKind, ContextValue, ErrorKind}, +}; + +#[derive(Clone)] +pub struct ShortcutValueParser(Vec); + +/// `ShortcutValueParser` is similar to clap's `PossibleValuesParser`: it verifies that the value is +/// from an enumerated set of `PossibleValue`. +/// +/// Whereas `PossibleValuesParser` only accepts exact matches, `ShortcutValueParser` also accepts +/// shortcuts as long as they are unambiguous. +impl ShortcutValueParser { + pub fn new(values: impl Into) -> Self { + values.into() + } +} + +impl TypedValueParser for ShortcutValueParser { + type Value = String; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let value = value + .to_str() + .ok_or(clap::Error::new(ErrorKind::InvalidUtf8))?; + + let matched_values: Vec<_> = self + .0 + .iter() + .filter(|x| x.get_name().starts_with(value)) + .collect(); + + if matched_values.len() == 1 { + Ok(matched_values[0].get_name().to_string()) + } else { + let mut err = clap::Error::new(ErrorKind::InvalidValue).with_cmd(cmd); + + if let Some(arg) = arg { + err.insert( + ContextKind::InvalidArg, + ContextValue::String(arg.to_string()), + ); + } + + err.insert( + ContextKind::InvalidValue, + ContextValue::String(value.to_string()), + ); + + err.insert( + ContextKind::ValidValue, + ContextValue::Strings(self.0.iter().map(|x| x.get_name().to_string()).collect()), + ); + + Err(err) + } + } + + fn possible_values(&self) -> Option + '_>> { + Some(Box::new(self.0.iter().cloned())) + } +} + +impl From for ShortcutValueParser +where + I: IntoIterator, + T: Into, +{ + fn from(values: I) -> Self { + Self(values.into_iter().map(|t| t.into()).collect()) + } +} + +#[cfg(test)] +mod tests { + use std::ffi::OsStr; + + use clap::{builder::TypedValueParser, error::ErrorKind, Command}; + + use super::ShortcutValueParser; + + #[test] + fn test_parse_ref() { + let cmd = Command::new("cmd"); + let parser = ShortcutValueParser::new(["abcd"]); + let values = ["a", "ab", "abc", "abcd"]; + + for value in values { + let result = parser.parse_ref(&cmd, None, OsStr::new(value)); + assert_eq!("abcd", result.unwrap()); + } + } + + #[test] + fn test_parse_ref_with_invalid_value() { + let cmd = Command::new("cmd"); + let parser = ShortcutValueParser::new(["abcd"]); + let invalid_values = ["e", "abe", "abcde"]; + + for invalid_value in invalid_values { + let result = parser.parse_ref(&cmd, None, OsStr::new(invalid_value)); + assert_eq!(ErrorKind::InvalidValue, result.unwrap_err().kind()); + } + } + + #[test] + fn test_parse_ref_with_ambiguous_value() { + let cmd = Command::new("cmd"); + let parser = ShortcutValueParser::new(["abcd", "abef"]); + let ambiguous_values = ["a", "ab"]; + + for ambiguous_value in ambiguous_values { + let result = parser.parse_ref(&cmd, None, OsStr::new(ambiguous_value)); + assert_eq!(ErrorKind::InvalidValue, result.unwrap_err().kind()); + } + + let result = parser.parse_ref(&cmd, None, OsStr::new("abc")); + assert_eq!("abcd", result.unwrap()); + + let result = parser.parse_ref(&cmd, None, OsStr::new("abe")); + assert_eq!("abef", result.unwrap()); + } + + #[test] + #[cfg(unix)] + fn test_parse_ref_with_invalid_utf8() { + use std::os::unix::prelude::OsStrExt; + + let parser = ShortcutValueParser::new(["abcd"]); + let cmd = Command::new("cmd"); + + let result = parser.parse_ref(&cmd, None, OsStr::from_bytes(&[0xc3 as u8, 0x28 as u8])); + assert_eq!(ErrorKind::InvalidUtf8, result.unwrap_err().kind()); + } +}