Skip to content
Merged
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
8 changes: 6 additions & 2 deletions xff/engine/run.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> tmpl = ResolveTemplate(command.globals);
// Precompile the --template once; rendering each match then skips re-scanning.
const std::optional<fields::Template> compiled_tmpl =
tmpl.has_value() ? std::optional<fields::Template>(fields::Template::Compile(*tmpl)) : std::nullopt;
if (expression != nullptr) {
ScanDepthOptions(*expression, &options);
}
Expand All @@ -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;
Expand Down
41 changes: 34 additions & 7 deletions xff/fields/fields.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
#include <filesystem>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "absl/time/time.h"
#include "xff/vfs/entry.h"
Expand Down Expand Up @@ -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
33 changes: 28 additions & 5 deletions xff/fields/fields.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,17 @@

#include <string>
#include <string_view>
#include <vector>

#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}
Expand All @@ -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<Segment> 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
Expand Down
6 changes: 6 additions & 0 deletions xff/fields/fields_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading