From 45525860ce131c101ac1071ac4068d660329cfa5 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:22:22 +0300 Subject: [PATCH 1/3] sort: reject conflicting -t separators The -t argument kept the default set-action together with args_override_self, so a later -t silently replaced an earlier one. Collect every occurrence and fail with "incompatible tabs" (exit 2) when they differ, the same way repeated -o is handled. Repeating the same separator keeps working. Fixes #15598 --- src/uu/sort/locales/en-US.ftl | 1 + src/uu/sort/locales/fr-FR.ftl | 1 + src/uu/sort/src/sort.rs | 20 +++++++++++++++++++- tests/by-util/test_sort.rs | 31 +++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/uu/sort/locales/en-US.ftl b/src/uu/sort/locales/en-US.ftl index 0f28d926de9..d951a1c3736 100644 --- a/src/uu/sort/locales/en-US.ftl +++ b/src/uu/sort/locales/en-US.ftl @@ -24,6 +24,7 @@ sort-file-operands-combined = extra operand {$file} file operands cannot be combined with --files0-from Try '{$help} --help' for more information. sort-multiple-output-files = multiple output files specified +sort-incompatible-tabs = incompatible tabs sort-minus-in-stdin = when reading file names from standard input, no file name of '-' allowed sort-no-input-from = no input from {$file} sort-random-source-end-of-file = {$path}: end of file diff --git a/src/uu/sort/locales/fr-FR.ftl b/src/uu/sort/locales/fr-FR.ftl index 064d02003f8..f891ee1910b 100644 --- a/src/uu/sort/locales/fr-FR.ftl +++ b/src/uu/sort/locales/fr-FR.ftl @@ -24,6 +24,7 @@ sort-file-operands-combined = opérande supplémentaire {$file} les opérandes de fichier ne peuvent pas être combinées avec --files0-from Essayez '{$help} --help' pour plus d'informations. sort-multiple-output-files = plusieurs fichiers de sortie spécifiés +sort-incompatible-tabs = tabulations incompatibles sort-minus-in-stdin = lors de la lecture des noms de fichiers depuis l'entrée standard, aucun nom de fichier '-' n'est autorisé sort-no-input-from = aucune entrée depuis {$file} sort-invalid-zero-length-filename = {$file}:{$line_num} : nom de fichier de longueur zéro invalide diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 584c5c9bee2..feba5b9d555 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -182,6 +182,9 @@ pub enum SortError { #[error("{}", translate!("sort-multiple-output-files"))] MultipleOutputFiles, + #[error("{}", translate!("sort-incompatible-tabs"))] + IncompatibleTabs, + #[error("{}", translate!("sort-minus-in-stdin"))] MinusInStdIn, @@ -2189,6 +2192,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(SortError::MultipleOutputFiles.into()); } + // Prevent -t/--field-separator from being given different values. Raw + // values suffice: `\0` is the only spelling rewritten later, and argv cannot hold a NUL. + if matches + .get_many::(options::SEPARATOR) + .is_some_and(|mut separators| { + separators + .next() + .is_some_and(|first| separators.any(|sep| sep != first)) + }) + { + return Err(SortError::IncompatibleTabs.into()); + } + settings.debug = matches.get_flag(options::DEBUG); if let Some(path) = matches.get_one::(options::RANDOM_SOURCE) { settings.random_source = Some(PathBuf::from(path)); @@ -2691,7 +2707,9 @@ pub fn uu_app() -> Command { .short('t') .long(options::SEPARATOR) .help(translate!("sort-help-separator")) - .value_parser(ValueParser::os_string()), + .value_parser(ValueParser::os_string()) + // To detect differing occurrences and raise an error + .action(ArgAction::Append), ) .arg( Arg::new(options::ZERO_TERMINATED) diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 28f5acd2b65..5188c868f88 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -2080,6 +2080,37 @@ fn test_separator_attached_equals_multi_char() { .stderr_contains("separator must be exactly one character long: '=a'"); } +#[test] +fn test_error_on_incompatible_separators() { + let cases: [&[&str]; 3] = [ + &["-t@", "-t!"], + &["-t", "@", "-t", "!"], + &["--field-separator=@", "-t", "!"], + ]; + for args in cases { + new_ucmd!() + .args(args) + .pipe_in("a@b\n") + .fails_with_code(2) + .stderr_only("sort: incompatible tabs\n"); + } +} + +#[test] +fn test_same_separator_twice_ok() { + let cases: [&[&str]; 2] = [ + &["-t:", "-t:", "-k2"], + &["--field-separator=:", "-t", ":", "-k2"], + ]; + for args in cases { + new_ucmd!() + .args(args) + .pipe_in("a:2\nb:1\n") + .succeeds() + .stdout_only("b:1\na:2\n"); + } +} + #[test] fn test_output_is_input() { let input = "a\nb\nc\n"; From 517a64e7b6a6a0f6cf90dc2940324512c2e23503 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:32:51 +0300 Subject: [PATCH 2/3] sort: extract shared option conflict helper --- src/uu/sort/src/sort.rs | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index feba5b9d555..1aeeae83567 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -2140,6 +2140,19 @@ fn emit_debug_warnings( } } +/// Whether option `id` was given several times with differing values. +/// Repeating an option is fine as long as every occurrence has the same value. +fn has_conflicting_values(matches: &ArgMatches, id: &str) -> bool +where + T: Clone + PartialEq + Send + Sync + 'static, +{ + matches.get_many::(id).is_some_and(|mut values| { + values + .next() + .is_some_and(|first| values.any(|value| value != first)) + }) +} + #[uucore::main] #[allow(clippy::cognitive_complexity)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -2180,28 +2193,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { uucore::clap_localization::handle_clap_result_with_exit_code(uu_app(), processed_args, 2)?; // Prevent -o/--output to be specified multiple times - if matches - .get_many::(options::OUTPUT) - .is_some_and(|mut outputs| { - outputs - .next() - .as_ref() - .is_some_and(|first| outputs.any(|out| out != *first)) - }) - { + if has_conflicting_values::(&matches, options::OUTPUT) { return Err(SortError::MultipleOutputFiles.into()); } // Prevent -t/--field-separator from being given different values. Raw // values suffice: `\0` is the only spelling rewritten later, and argv cannot hold a NUL. - if matches - .get_many::(options::SEPARATOR) - .is_some_and(|mut separators| { - separators - .next() - .is_some_and(|first| separators.any(|sep| sep != first)) - }) - { + if has_conflicting_values::(&matches, options::SEPARATOR) { return Err(SortError::IncompatibleTabs.into()); } From 6d181fa2d09ddbf609ad01fa1915c0236243a733 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:29:33 +0300 Subject: [PATCH 3/3] tests: sort: don't write stdin in early-error tests --- tests/by-util/test_sort.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 5188c868f88..7b4e9e34d7e 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -2090,7 +2090,6 @@ fn test_error_on_incompatible_separators() { for args in cases { new_ucmd!() .args(args) - .pipe_in("a@b\n") .fails_with_code(2) .stderr_only("sort: incompatible tabs\n"); }