diff --git a/src/uu/date/locales/en-US.ftl b/src/uu/date/locales/en-US.ftl index ea864904285..7d65baf34c2 100644 --- a/src/uu/date/locales/en-US.ftl +++ b/src/uu/date/locales/en-US.ftl @@ -110,3 +110,11 @@ date-error-format-modifier-width-too-large = format modifier width '{$width}' is date-error-format-missing-plus = the argument {$arg} lacks a leading '+'; when using an option to specify date(s), any non-option argument must be a format string beginning with '+' +date-error-invalid-choice = invalid argument '{$arg}' for '--{$option}' + Valid arguments are: + {$choices} + Try 'date --help' for more information. +date-error-ambiguous-choice = ambiguous argument '{$arg}' for '--{$option}' + Valid arguments are: + {$choices} + Try 'date --help' for more information. diff --git a/src/uu/date/locales/fr-FR.ftl b/src/uu/date/locales/fr-FR.ftl index 9a67704af1e..1feef81ff26 100644 --- a/src/uu/date/locales/fr-FR.ftl +++ b/src/uu/date/locales/fr-FR.ftl @@ -105,3 +105,11 @@ date-error-format-modifier-width-too-large = la largeur du modificateur de forma date-error-format-missing-plus = l'argument {$arg} ne commence pas par un signe '+'; lorsqu'une option est utilisée pour spécifier une ou plusieurs dates, tout argument autre qu'une option doit être une chaîne de format commençant par un signe '+'. +date-error-invalid-choice = argument '{$arg}' invalide pour '--{$option}' + Arguments valides : + {$choices} + Essayez 'date --help' pour plus d'informations. +date-error-ambiguous-choice = argument '{$arg}' ambigu pour '--{$option}' + Arguments valides : + {$choices} + Essayez 'date --help' pour plus d'informations. diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index f6296c48529..d472ed7cb35 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -30,8 +30,6 @@ use uucore::{format_usage, show}; #[cfg(windows)] use windows_sys::Win32::{Foundation::SYSTEMTIME, System::SystemInformation::SetSystemTime}; -use uucore::parser::shortcut_value_parser::ShortcutValueParser; - // Options const DATE: &str = "date"; const HOURS: &str = "hours"; @@ -125,7 +123,7 @@ impl From<&str> for Iso8601Format { SECONDS => Self::Seconds, NS => Self::Ns, DATE => Self::Date, - // Note: This is caught by clap via `possible_values` + // Note: only reached with a name `resolve_choice` already validated. _ => unreachable!(), } } @@ -143,12 +141,45 @@ impl From<&str> for Rfc3339Format { DATE => Self::Date, SECONDS => Self::Seconds, NS => Self::Ns, - // Should be caught by clap - _ => panic!("Invalid format: {s}"), + // Note: only reached with a name `resolve_choice` already validated. + _ => unreachable!(), } } } +/// The choices `--iso-8601`'s value accepts, in the order GNU lists them. +const ISO_8601_CHOICES: &[&str] = &[HOURS, MINUTES, DATE, SECONDS, NS]; + +/// The choices `--rfc-3339`'s value accepts, in the order GNU lists them. +const RFC_3339_CHOICES: &[&str] = &[DATE, SECONDS, NS]; + +/// The choice `value` names among `choices`, accepting any unambiguous +/// abbreviation the way GNU does. `option` is the long name to report the +/// error against if it names none, or more than one. +fn resolve_choice<'a>(value: &str, option: &'static str, choices: &[&'a str]) -> UResult<&'a str> { + let list = || { + choices + .iter() + .map(|name| format!(" - '{name}'")) + .collect::>() + .join("\n") + }; + let mut named = choices.iter().filter(|name| name.starts_with(value)); + match (named.next(), named.next()) { + // No choice abbreviates another, so a single match is the answer + // whether or not it is the whole word. + (Some(name), None) if !value.is_empty() => Ok(*name), + (Some(_), Some(_)) => Err(USimpleError::new( + 1, + translate!("date-error-ambiguous-choice", "arg" => value.to_string(), "option" => option, "choices" => list()), + )), + _ => Err(USimpleError::new( + 1, + translate!("date-error-invalid-choice", "arg" => value.to_string(), "option" => option, "choices" => list()), + )), + } +} + /// Indicates whether parsing a military timezone causes the date to remain the same, roll back to the previous day, or /// advance to the next day. /// This can occur when applying a military timezone with an optional hour offset crosses midnight @@ -348,16 +379,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Format::Custom(fmt) } else if let Some(fmt) = matches .get_many::(OPT_ISO_8601) - .map(|mut iter| iter.next().unwrap_or(&DATE.to_string()).as_str().into()) + .map(|mut iter| iter.next().map_or(DATE, String::as_str)) { - Format::Iso8601(fmt) + Format::Iso8601(resolve_choice(fmt, OPT_ISO_8601, ISO_8601_CHOICES)?.into()) } else if matches.get_flag(OPT_RFC_EMAIL) { Format::Rfc5322 - } else if let Some(fmt) = matches - .get_one::(OPT_RFC_3339) - .map(|s| s.as_str().into()) - { - Format::Rfc3339(fmt) + } else if let Some(fmt) = matches.get_one::(OPT_RFC_3339) { + Format::Rfc3339(resolve_choice(fmt, OPT_RFC_3339, RFC_3339_CHOICES)?.into()) } else if matches.get_flag(OPT_RESOLUTION) { Format::Resolution } else { @@ -657,9 +685,6 @@ pub fn uu_app() -> Command { .short('I') .long(OPT_ISO_8601) .value_name("FMT") - .value_parser(ShortcutValueParser::new([ - DATE, HOURS, MINUTES, SECONDS, NS, - ])) .num_args(0..=1) .default_missing_value(OPT_DATE) .help(translate!("date-help-iso-8601")), @@ -686,7 +711,6 @@ pub fn uu_app() -> Command { Arg::new(OPT_RFC_3339) .long(OPT_RFC_3339) .value_name("FMT") - .value_parser(ShortcutValueParser::new([DATE, SECONDS, NS])) .help(translate!("date-help-rfc-3339")), ) .arg( diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 17666fa3998..6f65a226817 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -248,6 +248,55 @@ fn test_date_rfc_3339_invalid_arg() { } } +#[test] +fn test_date_iso_8601_invalid_arg_message() { + new_ucmd!() + .arg("--iso-8601=foo") + .fails() + .stderr_is(concat!( + "date: invalid argument 'foo' for '--iso-8601'\n", + "Valid arguments are:\n", + " - 'hours'\n", + " - 'minutes'\n", + " - 'date'\n", + " - 'seconds'\n", + " - 'ns'\n", + "Try 'date --help' for more information.\n", + )); +} + +#[test] +fn test_date_iso_8601_ambiguous_arg_message() { + new_ucmd!() + .arg("--iso-8601=") + .fails() + .stderr_is(concat!( + "date: ambiguous argument '' for '--iso-8601'\n", + "Valid arguments are:\n", + " - 'hours'\n", + " - 'minutes'\n", + " - 'date'\n", + " - 'seconds'\n", + " - 'ns'\n", + "Try 'date --help' for more information.\n", + )); +} + +#[test] +fn test_date_rfc_3339_invalid_arg_message() { + new_ucmd!() + .arg("--rfc-3339=foo") + .fails() + .stderr_is(concat!( + "date: invalid argument 'foo' for '--rfc-3339'\n", + "Valid arguments are:\n", + " - 'date'\n", + " - 'seconds'\n", + " - 'ns'\n", + "Try 'date --help' for more information.\n", + )); +} + #[test] fn test_date_rfc_8601_default() { let re = Regex::new(r"^\d{4}-\d{2}-\d{2}\n$").unwrap();