From 9c8401d712b56d972b13938bd3df84dd134cd022 Mon Sep 17 00:00:00 2001 From: fixfon <12512757+fixfon@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:27:35 +0300 Subject: [PATCH 1/3] sort: reject conflicting --compress-program values --compress-program was a plain single-value option and sort uses args_override_self, so a later occurrence silently replaced the earlier one. Collect every occurrence like -o does and fail with exit status 2 when two of them differ. Repeating the same program still works. Fixes #15594 --- src/uu/sort/locales/en-US.ftl | 1 + src/uu/sort/locales/fr-FR.ftl | 1 + src/uu/sort/src/sort.rs | 15 ++++++++++++++- tests/by-util/test_sort.rs | 19 +++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/uu/sort/locales/en-US.ftl b/src/uu/sort/locales/en-US.ftl index 0f28d926de9..e0d2b914a93 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-multiple-compress-programs = multiple compress programs specified 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..ec2ebba2e23 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-multiple-compress-programs = plusieurs programmes de compression spécifiés 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..d348e4d39d3 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-multiple-compress-programs"))] + MultipleCompressPrograms, + #[error("{}", translate!("sort-minus-in-stdin"))] MinusInStdIn, @@ -2189,6 +2192,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return Err(SortError::MultipleOutputFiles.into()); } + // Same as -o: repeating --compress-program is fine as long as the value is the same + if let Some(mut progs) = matches.get_many::(options::COMPRESS_PROG) + && let Some(first) = progs.next() + && progs.any(|prog| prog != first) + { + return Err(SortError::MultipleCompressPrograms.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)); @@ -2727,7 +2738,9 @@ pub fn uu_app() -> Command { .long(options::COMPRESS_PROG) .help(translate!("sort-help-compress-prog")) .value_name("PROG") - .value_hint(clap::ValueHint::CommandName), + .value_hint(clap::ValueHint::CommandName) + // To detect multiple occurrences and raise an error + .action(ArgAction::Append), ) .arg( Arg::new(options::BATCH_SIZE) diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 28f5acd2b65..a493cad647b 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -2383,6 +2383,25 @@ fn test_same_output_flag_twice_ok() { .no_stderr(); } +#[test] +fn test_error_on_multiple_compress_programs() { + new_ucmd!() + .args(&["--compress-program", "pwd", "--compress-program", "python3"]) + .pipe_in("b\na\n") + .fails_with_code(2) + .stderr_only("sort: multiple compress programs specified\n"); +} + +#[test] +fn test_same_compress_program_twice_ok() { + // The input is far below the buffer size, so the program is never spawned. + new_ucmd!() + .args(&["--compress-program", "gzip", "--compress-program", "gzip"]) + .pipe_in("b\na\n") + .succeeds() + .stdout_only("a\nb\n"); +} + #[test] fn test_output_file_with_leading_dash() { let test_cases = [ From 85e645ef74457b1232dafe611257dd3ab33fbdfd 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 | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index d348e4d39d3..0b11cdb3bd0 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,23 +2193,12 @@ 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()); } // Same as -o: repeating --compress-program is fine as long as the value is the same - if let Some(mut progs) = matches.get_many::(options::COMPRESS_PROG) - && let Some(first) = progs.next() - && progs.any(|prog| prog != first) - { + if has_conflicting_values::(&matches, options::COMPRESS_PROG) { return Err(SortError::MultipleCompressPrograms.into()); } From 3f3ce8ac9cd282d8565d715919881a186157afd0 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 a493cad647b..695c4f1605d 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -2387,7 +2387,6 @@ fn test_same_output_flag_twice_ok() { fn test_error_on_multiple_compress_programs() { new_ucmd!() .args(&["--compress-program", "pwd", "--compress-program", "python3"]) - .pipe_in("b\na\n") .fails_with_code(2) .stderr_only("sort: multiple compress programs specified\n"); }