From 0677f28a96263bca85d1f4bd84e9e2cd49e07055 Mon Sep 17 00:00:00 2001 From: arbelonson-source <269032023+arbelonson-source@users.noreply.github.com> Date: Mon, 31 Aug 2026 06:14:08 +0300 Subject: [PATCH] date: match GNU's error for a bad --iso-8601 or --rfc-3339 value --iso-8601 and --rfc-3339 validated their value with a plain ShortcutValueParser, so an unrecognized or ambiguous value produced clap's own wording ("error: invalid value 'X' for '--iso-8601 []' ... [possible values: ...]") instead of GNU's ("date: invalid argument 'X' for '--iso-8601' / Valid arguments are: / ... / Try 'date --help' for more information."). Resolve the value against each option's own choice list by hand, accepting any unambiguous abbreviation the way GNU does, and report GNU's wording (and its distinct 'ambiguous argument' message) when it does not. AI-assisted-by: Claude Opus 5, via Claude Code --- src/uu/date/locales/en-US.ftl | 8 +++++ src/uu/date/locales/fr-FR.ftl | 8 +++++ src/uu/date/src/date.rs | 56 +++++++++++++++++++++++++---------- tests/by-util/test_date.rs | 49 ++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 16 deletions(-) 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();