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
68 changes: 52 additions & 16 deletions xff/engine/evaluate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -256,13 +256,38 @@ absl::Time PrintfTime(const vfs::Metadata& md, char which) {
}
}

// find's -printf FORMAT: expands % directives and \ escapes against the entry via
// the tables above. Supported %: p path, f name, h dir, s size, m octal perm, d
// depth, y type, i inode, n links, u/g owner name, U/G owner id; the time families
// a/c/t (asctime form) and Ak/Ck/Tk (strftime conversion k on atime/ctime/mtime),
// rendered in `tz`; %% literal; \: n t r \\ \0. Unknown directives/escapes are
// emitted literally.
std::string FormatPrintf(std::string_view format, const Visit& visit, absl::TimeZone tz) {
// A symlink's target for {target} (defined below with the other field helpers); read
// only when a -printf format actually references a field, so plain find -printf pays
// no readlink.
std::string LinkTarget(const EvalContext& ctx);

// Renders a -printf / -fprintf FORMAT against `ctx`'s entry. Expands find's % directives
// and \ escapes via the tables above. Supported %: p path, f name, h dir, s size, m octal
// perm, d depth, y type, i inode, n links, u/g owner name, U/G owner id; the time families
// a/c/t (asctime form) and Ak/Ck/Tk (strftime conversion k on atime/ctime/mtime), rendered
// in ctx.tz; %% literal; \: n t r \\ \0.
//
// xff: `%{NAME}` / `%{NAME:qualifier}` expands the brace field vocabulary -- the same
// fields as --format ({relpath} {core} {suffix} {target} {def.NAME} {env.NAME} {size:h},
// time qualifiers, the s/// rewrite, ...) -- so a per-entry action reaches fields find's %
// set does not name. A bare `{...}` stays literal (printf formats legitimately contain
// braces) and an unterminated `%{` is emitted literally, matching the field template's own
// lenient handling; the strict find style rejects `%{...}` before the walk (EnforceStyle).
// Unknown %/\ directives are emitted literally.
std::string FormatPrintf(std::string_view format, const EvalContext& ctx) {
const bool has_field = format.find("%{") != std::string_view::npos;
const std::string link = has_field ? LinkTarget(ctx) : std::string(); // backs {target}
const fields::RenderContext field_ctx{
.path = ctx.visit.path,
.root = ctx.visit.root,
.link_target = link,
.metadata = ctx.visit.metadata,
.depth = ctx.visit.depth,
.tz = ctx.tz,
.time_format = ctx.time_format,
.captures = ctx.captures,
.defines = ctx.defines,
.outputs = ctx.outputs};
std::string out;
for (std::string_view::size_type i = 0; i < format.size(); ++i) {
const char ch = format[i];
Expand All @@ -276,15 +301,26 @@ std::string FormatPrintf(std::string_view format, const Visit& visit, absl::Time
}
} else if (ch == '%' && i + 1 < format.size()) {
const char directive = format[++i];
if (directive == 'a' || directive == 'c' || directive == 't') {
absl::StrAppend(&out, datetime::FormatTime(PrintfTime(visit.metadata, directive), "asctime", tz));
if (directive == '{') {
// xff: %{NAME[:qualifier]} -> the brace field vocabulary. Read to the first '}'
// and render it as a single {field}; an unterminated %{ stays literal.
const std::string_view::size_type close = format.find('}', i + 1);
if (close == std::string_view::npos) {
out.append("%{");
} else {
const std::string_view inner = format.substr(i + 1, close - (i + 1));
absl::StrAppend(&out, fields::Render(absl::StrCat("{", inner, "}"), field_ctx));
i = close; // consume through the closing '}'
}
} else if (directive == 'a' || directive == 'c' || directive == 't') {
absl::StrAppend(&out, datetime::FormatTime(PrintfTime(ctx.visit.metadata, directive), "asctime", ctx.tz));
} else if ((directive == 'A' || directive == 'C' || directive == 'T') && i + 1 < format.size()) {
const char conv = format[++i]; // %Tk etc.: strftime conversion k on the chosen time
absl::StrAppend(
&out,
datetime::FormatTime(PrintfTime(visit.metadata, directive), absl::StrCat("%", std::string(1, conv)), tz));
&out, datetime::FormatTime(
PrintfTime(ctx.visit.metadata, directive), absl::StrCat("%", std::string(1, conv)), ctx.tz));
} else if (const auto it = kPrintfDirectives.find(directive); it != kPrintfDirectives.end()) {
it->second(out, visit);
it->second(out, ctx.visit);
} else {
out.push_back('%'); // unknown directive: emit the percent and char literally
out.push_back(directive);
Expand Down Expand Up @@ -1327,7 +1363,7 @@ bool EvalLs(const parser::Expr&, EvalContext& ctx) {

bool EvalPrintf(const parser::Expr& expr, EvalContext& ctx) {
if (!expr.args.empty()) {
ctx.emit(FormatPrintf(expr.args.front(), ctx.visit, ctx.tz)); // no implicit newline; the format owns it
ctx.emit(FormatPrintf(expr.args.front(), ctx)); // no implicit newline; the format owns it
}
return true;
}
Expand All @@ -1339,7 +1375,7 @@ bool EvalPrintln(const parser::Expr&, EvalContext& ctx) {

bool EvalPrintfln(const parser::Expr& expr, EvalContext& ctx) {
if (!expr.args.empty()) { // xff: -printf plus the OS line ending appended
ctx.emit(absl::StrCat(FormatPrintf(expr.args.front(), ctx.visit, ctx.tz), kOsLineEnding));
ctx.emit(absl::StrCat(FormatPrintf(expr.args.front(), ctx), kOsLineEnding));
}
return true;
}
Expand Down Expand Up @@ -1382,7 +1418,7 @@ bool EvalFls(const parser::Expr& expr, EvalContext& ctx) {
// -fprintf takes FILE then FORMAT; the format owns its own terminator, like -printf.
bool EvalFprintf(const parser::Expr& expr, EvalContext& ctx) {
if (expr.args.size() >= 2 && ctx.emit_file) {
ctx.emit_file(expr.args.front(), FormatPrintf(expr.args[1], ctx.visit, ctx.tz));
ctx.emit_file(expr.args.front(), FormatPrintf(expr.args[1], ctx));
}
return true;
}
Expand All @@ -1391,7 +1427,7 @@ bool EvalFprintf(const parser::Expr& expr, EvalContext& ctx) {
// FILE then FORMAT, like -fprintf.
bool EvalFprintfln(const parser::Expr& expr, EvalContext& ctx) {
if (expr.args.size() >= 2 && ctx.emit_file) {
ctx.emit_file(expr.args.front(), absl::StrCat(FormatPrintf(expr.args[1], ctx.visit, ctx.tz), kOsLineEnding));
ctx.emit_file(expr.args.front(), absl::StrCat(FormatPrintf(expr.args[1], ctx), kOsLineEnding));
}
return true;
}
Expand Down
11 changes: 11 additions & 0 deletions xff/engine/run_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,17 @@ TEST_F(RunTest, FprintlnAndFprintflnWriteWithOsLineEndingToFile) {
fs::remove(fln, ec);
}

TEST_F(RunTest, PrintfPercentBraceEscapeExpandsXffFields) {
// xff: `%{field}` in a -printf format reaches the brace field vocabulary (here
// {relpath}); `%%` stays a literal percent, a bare `{..}` stays literal (printf formats
// legitimately contain braces), and an unterminated `%{` is emitted literally. The whole
// format renders as one record (it owns its terminator).
EXPECT_THAT(
RunExpr({"-name", "a.txt", "-printf", "rel=%{relpath} f=%f pct=%% bare={x} bad=%{oops\n"}),
ElementsAre("rel=a.txt f=a.txt pct=% bare={x} bad=%{oops"));
EXPECT_THAT(last_errors_, 0);
}

TEST_F(RunTest, DaystartFeedsTheTimeTests) {
// Age a.txt to ~10 days ago, then select with -daystart -mtime +5 (older than
// ~5 days, measured from today's local midnight). 10 days clears the boundary
Expand Down
16 changes: 16 additions & 0 deletions xff/golden/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,19 @@ xff_golden(
setup = "testdata/basic_tree.sh",
xff_golden = "testdata/summary.xff.txt",
)

# The xff %{field} escape in -printf: find rejects it (it is an xff extension in an
# otherwise find-native action), xff expands it to the field vocabulary ({relpath}).
xff_golden(
name = "printf_field_test",
args = [
"<ROOT>",
"-type",
"f",
"-printf",
"%{relpath}\n",
],
find_golden = "testdata/printf_field.find.txt",
setup = "testdata/basic_tree.sh",
xff_golden = "testdata/printf_field.xff.txt",
)
4 changes: 3 additions & 1 deletion xff/golden/golden.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ def xff_golden(name, setup, args, find_golden, xff_golden, ordered = False, size
Args:
name: Test target name (should end in `_test`).
setup: A shell script (label/file) that populates the fixture tree ($PWD).
args: The xff expression argv (the search root is supplied by the driver).
args: The full xff argv; a `<ROOT>` token marks where the search root goes
(so globals like `--summary` can precede it). Use a real newline for a
-printf terminator, not `\\n` (the sh_test launcher strips the backslash).
find_golden: Expected normalized output under `--config=find`.
xff_golden: Expected normalized output under `--config=xff`.
ordered: Keep output line order (default: sort, so readdir order is immaterial).
Expand Down
1 change: 1 addition & 0 deletions xff/golden/testdata/printf_field.find.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xff: the '%{field}' escape in '-printf' is an xff extension, not available under the find style (--config=find); use --config=xff
3 changes: 3 additions & 0 deletions xff/golden/testdata/printf_field.xff.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
docs/c.txt
src/a.txt
src/b.log
34 changes: 34 additions & 0 deletions xff/parser/parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,34 @@ std::string_view FirstXffOperator(const Expr* expr) {
return FirstXffOperator(expr->rhs.get());
}

// Returns the first -printf / -fprintf (pre-order) whose FORMAT uses the xff `%{field}`
// escape, or nullptr. -printf / -fprintf are find-native actions, but %{...} -- the bridge
// from their % format into the brace field vocabulary -- is an xff extension the strict
// find style rejects. (-printfln / -fprintfln are xff extensions already, so they are
// caught by FirstXffExtension; -fprintf takes FILE then FORMAT, so its format is arg 1.)
const Expr* FirstXffPrintfField(const Expr* expr) {
if (expr == nullptr) {
return nullptr;
}
if (expr->kind == Expr::Kind::kPredicate) {
const registry::Descriptor* const descriptor = expr->descriptor;
if (descriptor != nullptr) {
const std::string_view name = descriptor->name;
const bool is_fprintf = name == "-fprintf";
const std::size_t fmt_index = is_fprintf ? 1 : 0;
if ((name == "-printf" || is_fprintf) && expr->args.size() > fmt_index
&& expr->args[fmt_index].find("%{") != std::string_view::npos) {
return expr;
}
}
return nullptr;
}
if (const Expr* const found = FirstXffPrintfField(expr->lhs.get()); found != nullptr) {
return found;
}
return FirstXffPrintfField(expr->rhs.get());
}

} // namespace

absl::StatusOr<Command> Parse(const std::vector<std::string>& args) {
Expand Down Expand Up @@ -485,6 +513,12 @@ absl::Status EnforceStyle(const Command& command, registry::Style style) {
"' (e.g. \"-3 weeks 3 hours\") is an xff extension, not available under the find style (--config=find); "
"use a day count, a unit suffix like -1h, or --config=xff"));
}
if (const Expr* const pf = FirstXffPrintfField(command.expression.get()); pf != nullptr) {
return absl::InvalidArgumentError(
absl::StrCat(
"the '%{field}' escape in '", pf->descriptor->name,
"' is an xff extension, not available under the find style (--config=find); use --config=xff"));
}
return absl::OkStatus();
}

Expand Down
13 changes: 13 additions & 0 deletions xff/parser/parser_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,19 @@ TEST_F(ParserTest, EnforceStyleRejectsFileWritingLineEndingActionsUnderFind) {
EXPECT_THAT(EnforceStyle(fln, registry::Style::kXff), IsOk());
}

TEST_F(ParserTest, EnforceStyleRejectsPrintfFieldEscapeUnderFind) {
// -printf / -fprintf are find-native, but their xff `%{field}` escape is not: the strict
// find style rejects a format that uses it (while a plain % format stays fine). -fprintf
// takes FILE then FORMAT, so the escape is checked in its second argument.
ASSERT_OK_AND_ASSIGN(const Command pf, Parse({".", "-printf", "%{relpath}\n"}));
EXPECT_THAT(EnforceStyle(pf, registry::Style::kFind), StatusIs(absl::StatusCode::kInvalidArgument));
ASSERT_OK_AND_ASSIGN(const Command fpf, Parse({".", "-fprintf", "out", "%{name}"}));
EXPECT_THAT(EnforceStyle(fpf, registry::Style::kFind), StatusIs(absl::StatusCode::kInvalidArgument));
ASSERT_OK_AND_ASSIGN(const Command plain, Parse({".", "-printf", "%p\n"}));
EXPECT_THAT(EnforceStyle(plain, registry::Style::kFind), IsOk());
EXPECT_THAT(EnforceStyle(pf, registry::Style::kXff), IsOk());
}

TEST_F(ParserTest, EnforceStyleWalksTheWholeTree) {
// A -capture buried under operators is still found (the check is a full walk).
ASSERT_OK_AND_ASSIGN(const Command cmd, Parse({".", "-type", "f", "-o", "-capture=n", "wc", ";"}));
Expand Down
2 changes: 1 addition & 1 deletion xff/registry/registry.cc
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ constexpr std::array kDescriptors = std::to_array<Descriptor>({
},
{
.name = "-printf",
.summary = "print a custom format string",
.summary = "print a custom format string (%{field} expands the xff field vocabulary)",
.kind = Kind::kAction,
.arity = 1,
},
Expand Down
Loading