From 24b979c8219f45ede57f304c5e6029619829cf15 Mon Sep 17 00:00:00 2001 From: Daniel Hofstetter Date: Wed, 14 Jun 2023 10:35:13 +0200 Subject: [PATCH] uucore: introduce ShortcutValueParser --- src/uucore/src/lib/lib.rs | 1 + src/uucore/src/lib/parser.rs | 1 + .../src/lib/parser/shortcut_value_parser.rs | 141 ++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 src/uucore/src/lib/parser/shortcut_value_parser.rs 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()); + } +}