From fa66097bb9632c1f0e1c0e49e0167fdc8e283db8 Mon Sep 17 00:00:00 2001 From: MadeNavaneeth <151734681+MadeNavaneeth@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:02:15 +0530 Subject: [PATCH] sort: handle clustered short options ending in attached '-t' GNU splits 'sort -nt=5' into flags '-n' plus '-t' taking '=5' verbatim, yielding the expected multi-character separator error. Clap instead stripped the leading '=', silently accepting '5' as a single-character separator. Extend the '-t=' rewrite: when a short-option cluster ends in 't' and every earlier character is a value-less flag, split into '-t' plus the remainder as a separate argument, which clap passes through unstripped. Value-taking shorts (-k/-o/-S) are never touched since only value-less flags precede the rewrite point. Follow-up to #14144 / #14120. --- src/uu/sort/src/sort.rs | 31 +++++++++++++++++++++++++++---- tests/by-util/test_sort.rs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 7a8bd550fd..6b9ca4ae37 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -2118,15 +2118,38 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // https://github.com/uutils/coreutils/issues/2424#issuecomment-863825242, // and the same rewrite in `cut`), so rewrite every attached `-t` // argument to its long form, which preserves the separator verbatim. - let args = args.into_iter().map(|x| { + let args = args.into_iter().flat_map(|x| { // Non-UTF-8 separators are rejected later anyway, so lossy conversion // here only affects arguments that cannot become a valid separator. let as_str = x.to_string_lossy(); + // Attached form -t: route through long option to preserve verbatim. if as_str.starts_with("-t") && as_str.chars().count() > 2 { - OsString::from(format!("--{}={}", options::SEPARATOR, &as_str[2..])) - } else { - x + return vec![OsString::from(format!( + "--{}={}", + options::SEPARATOR, + &as_str[2..] + ))]; + } + // Clustered form, e.g. -nt=5: split into -t + rest so clap + // passes the value through unstripped. Only for valueless flag clusters. + let valueless_shorts = "bCcdfghimMnRrsuz"; + let bytes = as_str.as_bytes(); + if bytes.len() > 3 + && bytes[0] == b'-' + && bytes[1] != b'-' + && let Some(t_idx) = (1..bytes.len() - 1).find(|&i| bytes[i] == b't') + { + let flags_before = &as_str[1..t_idx]; + if !flags_before.is_empty() + && flags_before.chars().all(|c| valueless_shorts.contains(c)) + { + return vec![ + OsString::from(format!("-{flags_before}t")), + OsString::from(&as_str[t_idx + 1..]), + ]; + } } + vec![x] }); let args: Vec = args.collect(); diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 52e6471e91..c1f1632d75 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -1819,12 +1819,43 @@ fn test_separator_attached_equals_double() { .stderr_contains("separator must be exactly one character long: '=='"); } +#[test] +fn test_separator_clustered_attached() { + // `-nt=5`: -n is a flag, -t takes the rest of the argument (`=5`) + // verbatim, which GNU rejects as multi-character. + new_ucmd!() + .args(&["-nt=5"]) + .pipe_in("a=b=c\n") + .fails() + .stderr_contains("'=5'"); +} + +#[test] +fn test_separator_clustered_attached_b() { + new_ucmd!() + .args(&["-bt=x"]) + .pipe_in("a=b=c\n") + .fails() + .stderr_contains("'=x'"); +} + +#[test] +fn test_separator_clustered_still_sorts() { + // The rewrite must not disturb value-taking shorts inside clusters' + // siblings: -n plus a working attached separator. #14120 + // Uses numeric field values so -n actually sorts. + new_ucmd!() + .args(&["-nt=", "-k", "2"]) + .pipe_in("3=b\n1=a\n2=c\n") + .succeeds() + .stdout_only("1=a\n2=c\n3=b\n"); +} + #[test] fn test_separator_attached_equals_multi_char() { // `-t=a` selects the two-character separator `=a`, which GNU rejects. new_ucmd!() .args(&["-t=a", "-k", "2"]) - .pipe_in("a=b=c\n") .fails() .stderr_contains("separator must be exactly one character long: '=a'"); }