Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2f5bd83
feat: Add --sort arg to CLI parsing
DeflateAwning Apr 20, 2026
6511a2f
chore: Add sort_key to Config struct
DeflateAwning Apr 20, 2026
bbca886
feat: Connect --sort CLI arg to work with -x
DeflateAwning Apr 20, 2026
d1364d1
chore(deps): Add rand dev dependency for shuffling in tests
DeflateAwning Apr 20, 2026
b30e1b4
test: Add test_sort_by_path and _with_exec
DeflateAwning Apr 20, 2026
3663a3c
docs: Add CHANGELOG entry
DeflateAwning Apr 20, 2026
5196835
fix: Clippy suggestions
DeflateAwning Apr 20, 2026
bf2435c
refactor: Move SortKey enum to config.rs (review comment)
DeflateAwning Apr 22, 2026
216b771
refactor,fix,test: Allow TestEnv configured to validate output order
DeflateAwning Apr 22, 2026
ba9cb06
perf,fix: Avoid collecting sorted results in batch if no sort required
DeflateAwning Apr 22, 2026
ab55454
Review suggestion - WorkerResult matching in sort - src/walk.rs
DeflateAwning Apr 22, 2026
f1553f4
fix: sort_worker_results error handling cases
DeflateAwning Apr 22, 2026
676be36
perf: Optimize sort_worker_results for cached key lookup (fn and orde…
DeflateAwning Apr 22, 2026
b62eb09
refactor: Use --sort mechanism with printing (until buffer full)
DeflateAwning Apr 22, 2026
61bd00a
refactor: Set max output buffer size in Config
DeflateAwning Apr 22, 2026
c861293
test: Create --sort=size test cases
DeflateAwning Apr 22, 2026
90e6539
docs: Explain --sort CLI arg better with warning
DeflateAwning Apr 22, 2026
86506ce
test: Add test_sort_by_size_with_exec_batch
DeflateAwning Apr 22, 2026
fee946e
fix: Clippy suggestions
DeflateAwning Apr 22, 2026
88ca1a6
fix: Directory file size shows differently across platforms
DeflateAwning Apr 23, 2026
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Features
- Add `--ignore-parent` option to override `--no-ignore-parent`, see #1958 (@tmchow)
- Add `--sort` arg to CLI to sort by path/size/dates, see #1875 and #1982 (@DeflateAwning)

## Bugfixes
- Handle invalid working directories gracefully when using `--full-path`, see #1900 (@Xavrir).
Expand Down
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ diff = "0.1"
tempfile = "3.27"
filetime = "0.2"
test-case = "3.3"
rand = "0.10.1"

[profile.dev]
debug = "line-tables-only"
Expand Down
16 changes: 15 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::exec::CommandSet;
use crate::filesystem;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::SizeFilter;
use crate::filter::{SizeFilter, SortKey};

#[derive(Parser)]
#[command(
Expand Down Expand Up @@ -549,6 +549,20 @@ pub struct Opts {
#[arg(long, hide = true, value_parser = parse_millis)]
pub max_buffer_time: Option<Duration>,

/// Sort search results by the given key before printing or executing commands.
///
/// Note: this buffers all results in memory before outputting.
Comment thread
DeflateAwning marked this conversation as resolved.
Outdated
#[arg(
long,
value_name = "key",
value_enum,
hide_short_help = true,
help = "Sort results by: path, size, created, or modified",
long_help,
conflicts_with("list_details")
)]
pub sort: Option<SortKey>,

///Limit the number of search results to 'count' and quit immediately.
#[arg(
long,
Expand Down
5 changes: 4 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::exec::CommandSet;
use crate::filetypes::FileTypes;
#[cfg(unix)]
use crate::filter::OwnerFilter;
use crate::filter::{SizeFilter, TimeFilter};
use crate::filter::{SizeFilter, SortKey, TimeFilter};
use crate::fmt::FormatTemplate;

/// Configuration options for *fd*.
Expand Down Expand Up @@ -133,6 +133,9 @@ pub struct Config {

/// Names that should stop traversal down their parent. (e.g. https://bford.info/cachedir/).
pub ignore_contain: Vec<String>,

/// The key to sort results by
pub sort_key: Option<SortKey>,
}

impl Config {
Expand Down
2 changes: 2 additions & 0 deletions src/filter/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
pub use self::size::SizeFilter;
pub use self::sort::SortKey;
pub use self::time::TimeFilter;

#[cfg(unix)]
pub use self::owner::OwnerFilter;

mod size;
mod sort; // Arguably not a "filter", but more of an augmentation on search results.
Comment thread
DeflateAwning marked this conversation as resolved.
Outdated
mod time;

#[cfg(unix)]
Expand Down
13 changes: 13 additions & 0 deletions src/filter/sort.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use clap::ValueEnum;

#[derive(Copy, Clone, PartialEq, Eq, Debug, ValueEnum)]
pub enum SortKey {
/// Sort by path
Path,
/// Sort by file size
Size,
/// Sort by creation time
Created,
/// Sort by modification time
Modified,
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ fn construct_config(mut opts: Opts, pattern_regexps: &[String]) -> Result<Config
max_results: opts.max_results(),
strip_cwd_prefix: opts.strip_cwd_prefix(|| !(opts.null_separator || has_command)),
ignore_contain: opts.ignore_contain,
sort_key: opts.sort,
})
}

Expand Down
43 changes: 42 additions & 1 deletion src/walk.rs
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.

This implementation requires sorting to be handled by every output path. That adds complexity and makes it harder to maintain.

It would be better if there is some way we could avoid needing separate sorting logic for --exec, --exec-batch, and printing.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use crate::error::print_error;
use crate::exec;
use crate::exit_codes::{ExitCode, merge_exitcodes};
use crate::filesystem;
use crate::filter::SortKey;
use crate::output;

/// The receiver thread can either be buffering results or directly streaming to the console.
Expand Down Expand Up @@ -411,7 +412,18 @@ impl WorkerState {
// This will be set to `Some` if the `--exec` argument was supplied.
if let Some(ref cmd) = config.command {
Comment thread
DeflateAwning marked this conversation as resolved.
if cmd.in_batch_mode() {
exec::batch(rx.into_iter().flatten(), cmd, config)
let mut results: Vec<WorkerResult> = rx.into_iter().flatten().collect();
Comment thread
DeflateAwning marked this conversation as resolved.
Outdated
if let Some(sort_key) = config.sort_key {
sort_worker_results(&mut results, sort_key);
}
exec::batch(results, cmd, config)
} else if let Some(sort_key) = config.sort_key {
// With --sort, we must collect all results before dispatching,
// and run sequentially so the order is preserved.

let mut results: Vec<WorkerResult> = rx.into_iter().flatten().collect();
sort_worker_results(&mut results, sort_key);
exec::job(results, cmd, config)
} else {
thread::scope(|scope| {
// Each spawned job will store its thread handle in here.
Expand Down Expand Up @@ -663,6 +675,35 @@ impl WorkerState {
}
}

fn sort_worker_results(results: &mut [WorkerResult], sort_key: SortKey) {
results.sort_by(|a, b| {
// Errors sort to the end; two errors are considered equal
let (WorkerResult::Entry(a), WorkerResult::Entry(b)) = (a, b) else {
return match (a, b) {
(WorkerResult::Error(_), WorkerResult::Entry(_)) => std::cmp::Ordering::Greater,
(WorkerResult::Entry(_), WorkerResult::Error(_)) => std::cmp::Ordering::Less,
_ => std::cmp::Ordering::Equal,
};
};
Comment thread
DeflateAwning marked this conversation as resolved.
Outdated

match sort_key {
Comment thread
DeflateAwning marked this conversation as resolved.
Outdated
SortKey::Path => a.path().cmp(b.path()),
SortKey::Size => {
let size = |e: &DirEntry| e.metadata().map(|m| m.len()).unwrap_or(0);
size(a).cmp(&size(b))
}
SortKey::Created => {
let time = |e: &DirEntry| e.metadata().and_then(|m| m.created().ok());
time(a).cmp(&time(b))
}
SortKey::Modified => {
let time = |e: &DirEntry| e.metadata().and_then(|m| m.modified().ok());
time(a).cmp(&time(b))
}
}
});
}

fn search_str_for_entry<'a>(
entry_path: &'a std::path::Path,
full_path_base: Option<&std::path::Path>,
Expand Down
43 changes: 43 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,49 @@ fn test_exec_batch_with_limit() {
);
}

fn shuffle_files(files: &[&'static str], seed: u64) -> Vec<&'static str> {
use rand::SeedableRng as _;
use rand::seq::SliceRandom as _;
let mut files = files.to_vec();
files.shuffle(&mut rand::rngs::StdRng::seed_from_u64(seed));

files
}

#[test]
fn test_sort_by_path() {
let te = TestEnv::new(DEFAULT_DIRS, &shuffle_files(DEFAULT_FILES, 42));

// --sort=path should produce deterministic alphabetical output
te.assert_output(
Comment thread
DeflateAwning marked this conversation as resolved.
&["--sort=path", "foo"],
"a.foo
one/b.foo
one/two/C.Foo2
one/two/c.foo
one/two/three/d.foo
one/two/three/directory_foo/",
);
}

/// Shell script execution with --sort (--exec)
#[cfg(not(windows))]
#[test]
fn test_sort_by_path_with_exec() {
let te = TestEnv::new(DEFAULT_DIRS, &shuffle_files(DEFAULT_FILES, 42));

// --exec with --sort should produce output in sorted order
te.assert_output(
&["foo", "--sort=path", "--exec", "echo", "File: {}"],
"File: ./a.foo
File: ./one/b.foo
File: ./one/two/C.Foo2
File: ./one/two/c.foo
File: ./one/two/three/d.foo
File: ./one/two/three/directory_foo",
);
}

/// Shell script execution (--exec) with a custom --path-separator
#[test]
fn test_exec_with_separator() {
Expand Down