From ff0f9258963b94cf4e089b7a372a8e701e1fbdef Mon Sep 17 00:00:00 2001 From: Yunus Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:38:49 +0300 Subject: [PATCH 1/3] sort: apply -d and -i to -V keys -V compared the raw key, so -d and -i were ignored, globally and per key. Remove the characters they ignore before comparing the key as a version. The filtered keys go into reused thread-local buffers, and a key is only copied when something is removed from it. --- src/uu/sort/src/custom_str_cmp.rs | 22 ++++++++++++++++++++++ src/uu/sort/src/sort.rs | 26 +++++++++++++++++++++++++- tests/by-util/test_sort.rs | 23 +++++++++++++++++++++++ 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/uu/sort/src/custom_str_cmp.rs b/src/uu/sort/src/custom_str_cmp.rs index d28195a943e..51811d8daa4 100644 --- a/src/uu/sort/src/custom_str_cmp.rs +++ b/src/uu/sort/src/custom_str_cmp.rs @@ -21,6 +21,28 @@ fn filter_char(c: u8, ignore_non_printing: bool, ignore_non_dictionary: bool) -> !(ignore_non_printing && (c.is_ascii_control() || !c.is_ascii())) } +/// Remove the bytes that `-i` and `-d` ignore, for comparisons that need the key itself. +/// The key is only copied, into `buf`, when something is removed. +pub fn filter_key<'a>( + s: &'a [u8], + ignore_non_printing: bool, + ignore_non_dictionary: bool, + buf: &'a mut Vec, +) -> &'a [u8] { + if s.iter() + .all(|&c| filter_char(c, ignore_non_printing, ignore_non_dictionary)) + { + return s; + } + buf.clear(); + buf.extend( + s.iter() + .copied() + .filter(|&c| filter_char(c, ignore_non_printing, ignore_non_dictionary)), + ); + buf +} + fn cmp_chars(a: u8, b: u8, ignore_case: bool) -> Ordering { if ignore_case { a.to_ascii_uppercase().cmp(&b.to_ascii_uppercase()) diff --git a/src/uu/sort/src/sort.rs b/src/uu/sort/src/sort.rs index 2e76ec78487..b6cadc1e181 100644 --- a/src/uu/sort/src/sort.rs +++ b/src/uu/sort/src/sort.rs @@ -24,7 +24,7 @@ use bigdecimal::BigDecimal; use chunks::LineData; use clap::builder::ValueParser; use clap::{Arg, ArgAction, ArgMatches, Command}; -use custom_str_cmp::{custom_str_cmp, is_blank}; +use custom_str_cmp::{custom_str_cmp, filter_key, is_blank}; use ext_sort::ext_sort; use foldhash::fast::FoldHasher; use foldhash::{HashMap, SharedSeed}; @@ -32,6 +32,7 @@ use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, nume use rand::{RngExt as _, rng}; #[cfg(not(target_os = "wasi"))] use rayon::slice::ParallelSliceMut; +use std::cell::RefCell; use std::cmp::Ordering; use std::env; use std::ffi::{OsStr, OsString}; @@ -2843,6 +2844,12 @@ fn compare_lines_with_collator(a: &Line<'_>, b: &Line<'_>, settings: &GlobalSett if settings.reverse { cmp.reverse() } else { cmp } } +thread_local! { + /// Reused for the `-d`/`-i` filtered keys of `-V`, so that comparing them doesn't allocate. + static VERSION_KEY_BUFFERS: RefCell<(Vec, Vec)> = + const { RefCell::new((Vec::new(), Vec::new())) }; +} + fn compare_by<'a>( a: &Line<'a>, b: &Line<'a>, @@ -2968,6 +2975,23 @@ fn compare_by<'a>( general_numeric_compare(a_float, b_float) } SortMode::Month => month_compare(a_str, b_str), + SortMode::Version if settings.dictionary_order || settings.ignore_non_printing => { + VERSION_KEY_BUFFERS.with_borrow_mut(|(a_buf, b_buf)| { + let a_key = filter_key( + a_str, + settings.ignore_non_printing, + settings.dictionary_order, + a_buf, + ); + let b_key = filter_key( + b_str, + settings.ignore_non_printing, + settings.dictionary_order, + b_buf, + ); + version_cmp(a_key, b_key) + }) + } SortMode::Version => version_cmp(a_str, b_str), SortMode::Default => { // Use locale-aware comparison if feature is enabled and no custom flags are set diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 0f149f517c4..6ebf61d6417 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -309,6 +309,29 @@ fn test_version_sort_stable() { .stdout_is("0.1\n0.02\n0.2\n0.002\n0.3\n"); } +#[test] +fn test_version_sort_dictionary_order_and_ignore_nonprinting() { + // -d and -i drop characters from the key before it is compared as a version + for args in [&["-dV"][..], &["-k1dV"], &["-k1,1dV"]] { + new_ucmd!() + .args(args) + .pipe_in("b\n_a\na10\na_9\n") + .succeeds() + .stdout_is("_a\na_9\na10\nb\n"); + } + new_ucmd!() + .arg("-iV") + .pipe_in("a10\na\x019\n") + .succeeds() + .stdout_is("a\x019\na10\n"); + // The key of the second line is empty, so it sorts before "0" + new_ucmd!() + .args(&["-c", "-iV"]) + .pipe_in("0\n\t\n") + .fails_with_code(1) + .stderr_only("sort: -:2: disorder: \t\n"); +} + #[test] fn test_ignore_case_orders_punctuation_after_letters() { new_ucmd!() From 6ce42def3c8fbd057b771392759c0dc6c538ca3e Mon Sep 17 00:00:00 2001 From: Yunus Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:12:24 +0300 Subject: [PATCH 2/3] sort: use rstest cases for the -V with -d/-i tests --- tests/by-util/test_sort.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index 6ebf61d6417..b049da10ff7 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -309,21 +309,26 @@ fn test_version_sort_stable() { .stdout_is("0.1\n0.02\n0.2\n0.002\n0.3\n"); } -#[test] -fn test_version_sort_dictionary_order_and_ignore_nonprinting() { - // -d and -i drop characters from the key before it is compared as a version - for args in [&["-dV"][..], &["-k1dV"], &["-k1,1dV"]] { - new_ucmd!() - .args(args) - .pipe_in("b\n_a\na10\na_9\n") - .succeeds() - .stdout_is("_a\na_9\na10\nb\n"); - } +// -d and -i drop characters from the key before it is compared as a version +#[rstest] +#[case::dictionary_order(&["-dV"], "b\n_a\na10\na_9\n", "_a\na_9\na10\nb\n")] +#[case::dictionary_order_key(&["-k1dV"], "b\n_a\na10\na_9\n", "_a\na_9\na10\nb\n")] +#[case::dictionary_order_bounded_key(&["-k1,1dV"], "b\n_a\na10\na_9\n", "_a\na_9\na10\nb\n")] +#[case::ignore_nonprinting(&["-iV"], "a10\na\x019\n", "a\x019\na10\n")] +fn test_version_sort_dictionary_order_and_ignore_nonprinting( + #[case] args: &[&str], + #[case] input: &str, + #[case] expected: &str, +) { new_ucmd!() - .arg("-iV") - .pipe_in("a10\na\x019\n") + .args(args) + .pipe_in(input) .succeeds() - .stdout_is("a\x019\na10\n"); + .stdout_is(expected); +} + +#[test] +fn test_version_sort_ignore_nonprinting_check() { // The key of the second line is empty, so it sorts before "0" new_ucmd!() .args(&["-c", "-iV"]) From 483e9c339c9a729c34c31dba10ab2b19f2c34097 Mon Sep 17 00:00:00 2001 From: Yunus Ozen <94426948+ozenyunus35@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:20:51 +0300 Subject: [PATCH 3/3] tests/sort: split the -V with -d/-i cases into separate tests --- tests/by-util/test_sort.rs | 48 +++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/tests/by-util/test_sort.rs b/tests/by-util/test_sort.rs index b049da10ff7..375a1f5ce0c 100644 --- a/tests/by-util/test_sort.rs +++ b/tests/by-util/test_sort.rs @@ -309,22 +309,42 @@ fn test_version_sort_stable() { .stdout_is("0.1\n0.02\n0.2\n0.002\n0.3\n"); } -// -d and -i drop characters from the key before it is compared as a version -#[rstest] -#[case::dictionary_order(&["-dV"], "b\n_a\na10\na_9\n", "_a\na_9\na10\nb\n")] -#[case::dictionary_order_key(&["-k1dV"], "b\n_a\na10\na_9\n", "_a\na_9\na10\nb\n")] -#[case::dictionary_order_bounded_key(&["-k1,1dV"], "b\n_a\na10\na_9\n", "_a\na_9\na10\nb\n")] -#[case::ignore_nonprinting(&["-iV"], "a10\na\x019\n", "a\x019\na10\n")] -fn test_version_sort_dictionary_order_and_ignore_nonprinting( - #[case] args: &[&str], - #[case] input: &str, - #[case] expected: &str, -) { +#[test] +fn test_version_sort_dictionary_order() { + // -d drops the underscores before the key is compared as a version new_ucmd!() - .args(args) - .pipe_in(input) + .arg("-dV") + .pipe_in("b\n_a\na10\na_9\n") .succeeds() - .stdout_is(expected); + .stdout_is("_a\na_9\na10\nb\n"); +} + +#[test] +fn test_version_sort_dictionary_order_key() { + new_ucmd!() + .arg("-k1dV") + .pipe_in("b\n_a\na10\na_9\n") + .succeeds() + .stdout_is("_a\na_9\na10\nb\n"); +} + +#[test] +fn test_version_sort_dictionary_order_bounded_key() { + new_ucmd!() + .arg("-k1,1dV") + .pipe_in("b\n_a\na10\na_9\n") + .succeeds() + .stdout_is("_a\na_9\na10\nb\n"); +} + +#[test] +fn test_version_sort_ignore_nonprinting() { + // -i drops the control character before the key is compared as a version + new_ucmd!() + .arg("-iV") + .pipe_in("a10\na\x019\n") + .succeeds() + .stdout_is("a\x019\na10\n"); } #[test]