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'"); }