diff --git a/xff/engine/run.cc b/xff/engine/run.cc index 034b8d3..d5a32ab 100644 --- a/xff/engine/run.cc +++ b/xff/engine/run.cc @@ -189,6 +189,9 @@ int RunFind(const parser::Command& command, const vfs::FileSystem& fs, EmitFn em options.symlinks = ResolveSymlinkMode(command.globals); const render::Format format = ResolveFormat(command.globals); const std::optional tmpl = ResolveTemplate(command.globals); + // Precompile the --template once; rendering each match then skips re-scanning. + const std::optional compiled_tmpl = + tmpl.has_value() ? std::optional(fields::Template::Compile(*tmpl)) : std::nullopt; if (expression != nullptr) { ScanDepthOptions(*expression, &options); } @@ -208,8 +211,9 @@ int RunFind(const parser::Command& command, const vfs::FileSystem& fs, EmitFn em Control control; const bool matched = expression == nullptr || Evaluate(*expression, visit, emit, walk_fs, now, control); if (matched && !has_action) { - emit(tmpl.has_value() ? fields::Render(*tmpl, visit.path, visit.metadata, visit.depth) + "\n" - : render::Renderer(format).Record(visit.path)); // --template overrides --format + emit(compiled_tmpl.has_value() + ? compiled_tmpl->Render(visit.path, visit.metadata, visit.depth) + "\n" + : render::Renderer(format).Record(visit.path)); // --template overrides --format } if (control.quit) return WalkAction::kStop; if (control.prune) return WalkAction::kPrune; diff --git a/xff/fields/fields.cc b/xff/fields/fields.cc index 6d8a776..4e9403b 100644 --- a/xff/fields/fields.cc +++ b/xff/fields/fields.cc @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include "absl/time/time.h" #include "xff/vfs/entry.h" @@ -197,33 +199,58 @@ std::string_view::size_type ParseField( } // namespace -std::string Render(std::string_view tmpl, std::string_view path, const vfs::Metadata& metadata, int depth) { - std::string out; +Template Template::Compile(std::string_view tmpl) { + Template compiled; + std::string literal; + const auto flush_literal = [&] { + if (!literal.empty()) { + compiled.segments_.push_back({/*is_field=*/false, std::move(literal), {}}); + literal.clear(); // restore the moved-from buffer to a known-empty state + } + }; for (std::string_view::size_type i = 0; i < tmpl.size();) { const char ch = tmpl[i]; if (ch == '{' && i + 1 < tmpl.size() && tmpl[i + 1] == '{') { - out.push_back('{'); + literal.push_back('{'); i += 2; } else if (ch == '}' && i + 1 < tmpl.size() && tmpl[i + 1] == '}') { - out.push_back('}'); + literal.push_back('}'); i += 2; } else if (ch == '{') { std::string_view name; std::string qualifier; const std::string_view::size_type next = ParseField(tmpl, i, name, qualifier); if (next == std::string_view::npos) { // not a well-formed placeholder -> literal '{' - out.push_back(ch); + literal.push_back(ch); ++i; continue; } - out.append(ResolveField(name, qualifier, path, metadata, depth)); + flush_literal(); + compiled.segments_.push_back({/*is_field=*/true, std::string(name), std::move(qualifier)}); i = next; } else { - out.push_back(ch); + literal.push_back(ch); ++i; } } + flush_literal(); + return compiled; +} + +std::string Template::Render(std::string_view path, const vfs::Metadata& metadata, int depth) const { + std::string out; + for (const Segment& segment : segments_) { + if (segment.is_field) { + out.append(ResolveField(segment.text, segment.qualifier, path, metadata, depth)); + } else { + out.append(segment.text); + } + } return out; } +std::string Render(std::string_view tmpl, std::string_view path, const vfs::Metadata& metadata, int depth) { + return Template::Compile(tmpl).Render(path, metadata, depth); +} + } // namespace xff::fields diff --git a/xff/fields/fields.h b/xff/fields/fields.h index 3c65af3..6c962b1 100644 --- a/xff/fields/fields.h +++ b/xff/fields/fields.h @@ -18,16 +18,17 @@ #include #include +#include #include "xff/vfs/entry.h" namespace xff::fields { -// Renders `tmpl` against one entry, substituting {field} placeholders with -// values derived from `path`, `metadata`, and `depth`. `{{` and `}}` emit -// literal braces; an unterminated `{` is literal and an unknown field renders -// empty. This is the foundation for the --format template and (gated) -exec -// substitution. +// Renders {field} placeholder templates against a visited entry, substituting +// values derived from its `path`, `metadata`, and `depth`. `{{` and `}}` emit +// literal braces; an unterminated or malformed `{` stays literal; an unknown +// field renders empty. This backs the --format/--template output and (gated) +// -exec substitution. // // Supported: {path} {dir} {name}/{file} {stem} {ext}/{extension} {suffixes} // {depth} {size} ({size:h} human-readable) {type} {inode} {links} {mode}/{perm} @@ -36,6 +37,28 @@ namespace xff::fields { // preset ({mtime:iso|epoch}); local time, default ISO-8601. A qualifier may be // written as a "C-quoted string" ({mtime:"{\"t\":\"%H:%M\"}"}) so it can hold a // literal '}' or ':' (\" and \\ are escapes). {root} layers on later. +// +// Compile parses the template once into literal/field segments; the resulting +// Template renders against many entries without re-scanning -- the hot path for +// --template (and -exec), which render every match. +class Template { + public: + static Template Compile(std::string_view tmpl); + + std::string Render(std::string_view path, const vfs::Metadata& metadata, int depth) const; + + private: + struct Segment { + bool is_field = false; // false: emit `text` verbatim; true: resolve field `text` + std::string text; // literal run, or field name when is_field + std::string qualifier; // field qualifier, when is_field + }; + + std::vector segments_; +}; + +// Convenience wrapper: Compile(tmpl).Render(...). Prefer Compile once + Render +// per entry on hot paths. std::string Render(std::string_view tmpl, std::string_view path, const vfs::Metadata& metadata, int depth); } // namespace xff::fields diff --git a/xff/fields/fields_test.cc b/xff/fields/fields_test.cc index b24e512..0d085c0 100644 --- a/xff/fields/fields_test.cc +++ b/xff/fields/fields_test.cc @@ -94,5 +94,11 @@ TEST_F(FieldsTest, QuotedQualifierCarriesBracesColonsAndQuotes) { EXPECT_THAT(Render(R"({mtime:"%Y)", "f", md, 0), Eq(R"({mtime:"%Y)")); } +TEST_F(FieldsTest, CompiledTemplateRendersManyEntries) { + const Template compiled = Template::Compile("{name}={size:h}"); // parsed once, reused below + EXPECT_THAT(compiled.Render("a/x", Meta(vfs::FileType::kRegular, 1), 0), Eq("x=1")); + EXPECT_THAT(compiled.Render("b/big", Meta(vfs::FileType::kRegular, 1536), 0), Eq("big=1.5K")); +} + } // namespace } // namespace xff::fields