Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/uu/sort/locales/en-US.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/uu/sort/locales/fr-FR.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 26 additions & 10 deletions src/uu/sort/src/sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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<T>(matches: &ArgMatches, id: &str) -> bool
where
T: Clone + PartialEq + Send + Sync + 'static,
{
matches.get_many::<T>(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<()> {
Expand Down Expand Up @@ -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::<OsString>(options::OUTPUT)
.is_some_and(|mut outputs| {
outputs
.next()
.as_ref()
.is_some_and(|first| outputs.any(|out| out != *first))
})
{
if has_conflicting_values::<OsString>(&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::<OsString>(&matches, options::SEPARATOR) {
return Err(SortError::IncompatibleTabs.into());
}

settings.debug = matches.get_flag(options::DEBUG);
if let Some(path) = matches.get_one::<OsString>(options::RANDOM_SOURCE) {
settings.random_source = Some(PathBuf::from(path));
Expand Down Expand Up @@ -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)
Expand Down
30 changes: 30 additions & 0 deletions tests/by-util/test_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [

@xtqqczze xtqqczze Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fixfon could you please follow-up by converting the test cases to #[rstest] cases

&["-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";
Expand Down
Loading