From 8d001d2ad0d4af6ad008b3acb1e2a34265980275 Mon Sep 17 00:00:00 2001 From: helly25 <6420169+helly25@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:43:16 +0100 Subject: [PATCH] feat(engine): --sort=name for deterministic sibling ordering Add --sort[=name], an xff extension: order each directory's entries by name before visiting, so the walk is reproducible and diffable instead of filesystem-readdir order (find's default, which --sort=none / absence preserves). Bare --sort means --sort=name; last occurrence wins. - WalkOptions gains SortOrder { kNone, kName }; Walker::Descend sorts the ReadDir result by path (siblings share the dir prefix, so path order is name order) when kName, covering both pre- and post-order. - run.cc ResolveSort(globals) maps --sort / --sort=name -> kName, --sort=none -> kNone. Tests: run_test (--sort visits root, a.txt < b.md < sub, sub/c.txt in that exact order via ElementsAre; the unsorted default stays UnorderedElementsAre). Green in both configs; clang-format + em-dash clean. The parallel-traversal half of #43 remains a separate follow-up. --- xff/engine/BUILD.bazel | 1 + xff/engine/run.cc | 16 ++++++++++++++++ xff/engine/run_test.cc | 20 ++++++++++++++++++++ xff/engine/walk.cc | 8 +++++++- xff/engine/walk.h | 7 +++++++ 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/xff/engine/BUILD.bazel b/xff/engine/BUILD.bazel index 10c7683..2e144ff 100644 --- a/xff/engine/BUILD.bazel +++ b/xff/engine/BUILD.bazel @@ -23,6 +23,7 @@ cc_library( visibility = ["//xff:__subpackages__"], deps = [ "//xff/vfs:vfs_cc", + "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/functional:function_ref", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", diff --git a/xff/engine/run.cc b/xff/engine/run.cc index db895ef..b20d276 100644 --- a/xff/engine/run.cc +++ b/xff/engine/run.cc @@ -104,6 +104,21 @@ SymlinkMode ResolveSymlinkMode(const std::vector& globals) { return mode; } +// xff --sort[=name|none]: order siblings by name within each directory for +// deterministic output, or keep readdir order (none / absent). Bare --sort means +// --sort=name. Leading global, last occurrence wins. +SortOrder ResolveSort(const std::vector& globals) { + SortOrder sort = SortOrder::kNone; + for (const std::string& global : globals) { + if (global == "--sort" || global == "--sort=name") { + sort = SortOrder::kName; + } else if (global == "--sort=none") { + sort = SortOrder::kNone; + } + } + return sort; +} + // xff's modern output selector (leading globals, last wins, default plain): // --format=plain|nul|jsonl, with -0 a shorthand for NUL. find's -print/-print0 // keep their fixed formats; this drives only the implicit (default) print. @@ -368,6 +383,7 @@ int RunFind(const parser::Command& command, const vfs::FileSystem& fs, EmitFn em } WalkOptions options; options.symlinks = ResolveSymlinkMode(command.globals); + options.sort = ResolveSort(command.globals); const render::Format format = ResolveFormat(command.globals); const std::optional tmpl = ResolveTemplate(command.globals); // A -capture whose {capture.NAME} is never referenced ran a subprocess for diff --git a/xff/engine/run_test.cc b/xff/engine/run_test.cc index 2d4c7e5..c02a996 100644 --- a/xff/engine/run_test.cc +++ b/xff/engine/run_test.cc @@ -98,6 +98,26 @@ TEST_F(RunTest, NoExpressionPrintsEverything) { EXPECT_THAT(last_errors_, 0); } +TEST_F(RunTest, SortNameVisitsSiblingsInDeterministicOrder) { + // --sort=name orders each directory's entries by name, so the whole walk is + // deterministic: root first, then a.txt < b.md < sub, then sub/c.txt. ElementsAre + // (not UnorderedElementsAre) asserts the exact sequence. + const auto command = parser::Parse({"--sort", root_.string()}); + ASSERT_THAT(command, IsOk()); + std::vector records; + RunFind( + *command, fs_, + [&](std::string_view record) { + std::string text(record); + if (!text.empty() && text.back() == '\n') { + text.pop_back(); + } + records.push_back(std::move(text)); + }, + [](std::string_view, absl::Status) {}); + EXPECT_THAT(records, ElementsAre(root_.string(), Path("a.txt"), Path("b.md"), Path("sub"), Path("sub/c.txt"))); +} + TEST_F(RunTest, NameGlobImplicitPrint) { EXPECT_THAT(RunExpr({"-name", "*.txt"}), UnorderedElementsAre(Path("a.txt"), Path("sub/c.txt"))); } diff --git a/xff/engine/walk.cc b/xff/engine/walk.cc index d745843..a356f61 100644 --- a/xff/engine/walk.cc +++ b/xff/engine/walk.cc @@ -22,6 +22,7 @@ #include #include +#include "absl/algorithm/container.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/types/span.h" @@ -114,11 +115,16 @@ class Walker { } void Descend(const std::string& dir, int depth) { - const absl::StatusOr> children = fs_.ReadDir(dir); + absl::StatusOr> children = fs_.ReadDir(dir); if (!children.ok()) { on_error_(dir, children.status()); return; } + // --sort=name: order siblings by path before visiting. They share `dir` as a + // prefix, so path order is name order; the result is a deterministic walk. + if (options_.sort == SortOrder::kName) { + absl::c_sort(*children, [](const vfs::Entry& a, const vfs::Entry& b) { return a.path < b.path; }); + } for (const vfs::Entry& child : *children) { if (stopped_) { return; diff --git a/xff/engine/walk.h b/xff/engine/walk.h index 2f2dc00..52682f3 100644 --- a/xff/engine/walk.h +++ b/xff/engine/walk.h @@ -32,6 +32,11 @@ namespace xff::engine { // (`-L`). Following enables filesystem-loop detection. enum class SymlinkMode { kNever, kRoots, kAll }; +// Sibling ordering within each directory (xff's --sort). kNone keeps the +// filesystem's readdir order (find's default, fastest, non-reproducible); kName +// sorts each directory's entries by path so output is deterministic and diffable. +enum class SortOrder { kNone, kName }; + // Traversal limits. Parallelism is layered on in a follow-up (design.md // "Determinism"). struct WalkOptions { @@ -52,6 +57,8 @@ struct WalkOptions { // symlink is followed, its target's metadata is reported and a directory target // is descended into, with loop detection. SymlinkMode symlinks = SymlinkMode::kNever; + // Sibling ordering within each directory (xff `--sort`); kNone is readdir order. + SortOrder sort = SortOrder::kNone; }; // One visited entry handed to the `Visitor`. `path`/`name` reference storage