Skip to content
Open
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
22 changes: 22 additions & 0 deletions src/uu/sort/src/custom_str_cmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>,
) -> &'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())
Expand Down
26 changes: 25 additions & 1 deletion src/uu/sort/src/sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ 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};
use numeric_str_cmp::{NumInfo, NumInfoParseSettings, human_numeric_str_cmp, numeric_str_cmp};
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};
Expand Down Expand Up @@ -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<u8>, Vec<u8>)> =
const { RefCell::new((Vec::new(), Vec::new())) };
}

fn compare_by<'a>(
a: &Line<'a>,
b: &Line<'a>,
Expand Down Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions tests/by-util/test_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,54 @@ 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() {
// -d drops the underscores before the key is compared as a version
new_ucmd!()
.arg("-dV")
.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_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]
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"])
.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!()
Expand Down
Loading