From 97ce1a056f0108e20addef16a93f2b0c7af24737 Mon Sep 17 00:00:00 2001 From: arbelonson-source <269032023+arbelonson-source@users.noreply.github.com> Date: Mon, 31 Aug 2026 06:53:31 +0300 Subject: [PATCH] cut: match GNU's error for a bad --whitespace-delimited value --whitespace-delimited validated its value with a plain ShortcutValueParser, so an unrecognized value produced clap's own generic wording instead of GNU's ('invalid argument ... for --whitespace-delimited / Valid arguments are: / - 'trimmed' / ...'). Resolve the value against the option's own (single-choice) list by hand, accepting any unambiguous abbreviation the way GNU does -- including the empty string, e.g. --whitespace-delimited=: with only one choice, the empty string (a prefix of every choice, same as the other options fixed in this series) names it unambiguously, so it resolves to 'trimmed' rather than erroring. An existing test (test_whitespace_delimited_long_and_trimmed) already asserted this exact behavior for '' and caught my first attempt, which mirrored the other fixes' guard against a *genuinely* ambiguous empty value and wrongly rejected it here where there is nothing to be ambiguous with. AI-assisted-by: Claude Opus 5, via Claude Code --- src/uu/cut/locales/en-US.ftl | 8 ++++++++ src/uu/cut/locales/fr-FR.ftl | 8 ++++++++ src/uu/cut/src/cut.rs | 37 +++++++++++++++++++++++++++++++++--- tests/by-util/test_cut.rs | 8 +++++++- 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/uu/cut/locales/en-US.ftl b/src/uu/cut/locales/en-US.ftl index 6e63feb2638..cbaeee5b4a2 100644 --- a/src/uu/cut/locales/en-US.ftl +++ b/src/uu/cut/locales/en-US.ftl @@ -48,6 +48,14 @@ cut-error-invalid-field-value = invalid field value { $value } cut-error-invalid-position-value = invalid byte/character position { $value } cut-error-field-number-too-large = field number { $value } is too large cut-error-position-too-large = byte/character offset { $value } is too large +cut-error-invalid-whitespace-delimited-choice = invalid argument '{ $arg }' for '--whitespace-delimited' + Valid arguments are: + { $choices } + Try 'cut --help' for more information. +cut-error-ambiguous-whitespace-delimited-choice = ambiguous argument '{ $arg }' for '--whitespace-delimited' + Valid arguments are: + { $choices } + Try 'cut --help' for more information. # Diagnostic labels: what the caret points at in a list of ranges cut-diag-label-zero-bound = counting starts at 1 diff --git a/src/uu/cut/locales/fr-FR.ftl b/src/uu/cut/locales/fr-FR.ftl index 8428bb9c0fc..d60b28649b2 100644 --- a/src/uu/cut/locales/fr-FR.ftl +++ b/src/uu/cut/locales/fr-FR.ftl @@ -123,6 +123,14 @@ cut-error-invalid-field-value = valeur de champ invalide { $value } cut-error-invalid-position-value = position d'octet/caractère invalide { $value } cut-error-field-number-too-large = le numéro de champ { $value } est trop grand cut-error-position-too-large = le décalage d'octet/caractère { $value } est trop grand +cut-error-invalid-whitespace-delimited-choice = argument '{ $arg }' invalide pour '--whitespace-delimited' + Les arguments valides sont : + { $choices } + Essayez 'cut --help' pour plus d'informations. +cut-error-ambiguous-whitespace-delimited-choice = argument '{ $arg }' ambigu pour '--whitespace-delimited' + Les arguments valides sont : + { $choices } + Essayez 'cut --help' pour plus d'informations. # Étiquettes de diagnostic : ce que le caret désigne dans une liste d'intervalles cut-diag-label-zero-bound = le décompte commence à 1 diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 8d31ae493af..f405af538e3 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (ToDO) delim foxjumping sourcefiles undelimited xacfoxjumping use bstr::io::BufReadExt; -use clap::builder::{PossibleValue, ValueParser}; +use clap::builder::ValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; use std::ffi::OsString; use std::fs::File; @@ -17,7 +17,6 @@ use uucore::error::{FromIo, UResult, USimpleError, UUsageError, set_exit_code, s use uucore::i18n::charmap::{Encoding, locale_encoding, mb_char_len}; use uucore::line_ending::LineEnding; use uucore::os_str_as_bytes; -use uucore::parser::shortcut_value_parser::ShortcutValueParser; use self::searcher::Searcher; use matcher::{ExactMatcher, Matcher, MbExactMatcher, WhitespaceMatcher}; @@ -968,6 +967,37 @@ where ); } +/// The choices `--whitespace-delimited`'s value accepts. +const WHITESPACE_DELIMITED_CHOICES: &[&str] = &["trimmed"]; + +/// The choice `value` names among `WHITESPACE_DELIMITED_CHOICES`, accepting +/// any unambiguous abbreviation the way GNU does -- including the empty +/// string, which (like everywhere else here) is a prefix of every choice +/// but, with only one choice to begin with, names it unambiguously. +fn resolve_whitespace_delimited_choice(value: &str) -> UResult<&'static str> { + let list = || { + WHITESPACE_DELIMITED_CHOICES + .iter() + .map(|name| format!(" - '{name}'")) + .collect::>() + .join("\n") + }; + let mut named = WHITESPACE_DELIMITED_CHOICES + .iter() + .filter(|name| name.starts_with(value)); + match (named.next(), named.next()) { + (Some(name), None) => Ok(*name), + (Some(_), Some(_)) => Err(USimpleError::new( + 1, + translate!("cut-error-ambiguous-whitespace-delimited-choice", "arg" => value.to_string(), "choices" => list()), + )), + _ => Err(USimpleError::new( + 1, + translate!("cut-error-invalid-whitespace-delimited-choice", "arg" => value.to_string(), "choices" => list()), + )), + } +} + /// Get delimiter and output delimiter from `-d`/`--delimiter` and `--output-delimiter` options respectively /// Allow either delimiter to have a value that is neither UTF-8 nor ASCII to align with GNU behavior fn get_delimiters(matches: &ArgMatches) -> UResult<(Delimiter<'_>, Option<&[u8]>)> { @@ -1065,6 +1095,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // `--whitespace-delimited[=trimmed]` (`-w`): the optional value selects trimming. let whitespace_trimmed = matches .get_one::(options::WHITESPACE_DELIMITED) + .map(|value| resolve_whitespace_delimited_choice(value)) + .transpose()? .is_some(); let mode_arg = get_mode_arg(&matches)?; @@ -1276,7 +1308,6 @@ pub fn uu_app() -> Command { .long(options::WHITESPACE_DELIMITED) .help(translate!("cut-help-whitespace-delimited")) .value_name("trimmed") - .value_parser(ShortcutValueParser::new([PossibleValue::new("trimmed")])) .num_args(0..=1) .require_equals(true) .action(ArgAction::Set), diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index 9c2c7b3c3e6..2cd31ad7d03 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -746,7 +746,13 @@ fn test_whitespace_delimited_long_and_trimmed() { // Only `trimmed` is a valid value. new_ucmd!() .args(&["--whitespace-delimited=middle", "-f1"]) - .fails_with_code(1); + .fails_with_code(1) + .stderr_is(concat!( + "cut: invalid argument 'middle' for '--whitespace-delimited'\n", + "Valid arguments are:\n", + " - 'trimmed'\n", + "Try 'cut --help' for more information.\n", + )); } #[test]