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..1aeeae83567 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, @@ -2137,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<()> { @@ -2177,18 +2193,16 @@ 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 has_conflicting_values::(&matches, options::SEPARATOR) { + 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 +2705,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..7b4e9e34d7e 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -2080,6 +2080,36 @@ 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) + .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";