Author a custom DSL filter¶
components/dsl is a declarative, user-extensible text-filter engine (adapted from rtk), wrapped by
the cmdfilter component. Filters are authored in YAML — no
recompile — matched first-by-sorted-name, and each runs a fixed 8-stage pipeline. Because filters
drop lines they are lossy, which is why the wrapping cmdfilter is an
Offload: it stashes the original first and appends a
<<cg:HASH>> recovery hint only when the filter was actually lossy.
The 8-stage pipeline¶
A matched filter runs its input through these stages in order:
flowchart LR
I[input] --> S1[1 strip_ansi] --> S2["2 replace[]"] --> S3["3 match_output[] + unless"]
S3 --> S4[4 strip / keep lines] --> S5[5 truncate_lines_at] --> S6[6 head / tail]
S6 --> S7[7 max_lines] --> S8[8 on_empty] --> O[output + Lossiness]
The engine reports Lossiness back to cmdfilter, which drives the recovery hint: None (nothing
dropped / reversible reformat), Tail (a clean contiguous tail dropped), Whole (non-contiguous or
whole-blob loss).
Filter fields¶
All optional except match.
| Field | Purpose |
|---|---|
match |
regex tested against the selector (the output's first non-empty line) — decides if this filter applies |
strip_ansi |
strip ANSI escape codes |
replace |
chained pattern → replacement substitutions, $1 backrefs |
match_output |
whole-blob short-circuit: pattern / message / unless |
strip_lines_matching xor keep_lines_matching |
drop, or keep-only, lines matching these regexes (mutually exclusive) |
truncate_lines_at |
per-line character cap |
head_lines / tail_lines |
keep the first / last N lines |
max_lines |
absolute line cap with an omission marker |
on_empty |
replacement text when the output ends up blank |
strip xor keep
strip_lines_matching and keep_lines_matching are mutually exclusive — set one or the other,
never both.
A full example (pytest)¶
Documents load with schema_version: 1 and strict unknown-field rejection.
schema_version: 1
filters:
pytest:
description: keep failures + summary, drop passing noise
match: "(pytest|=+ test session starts)"
strip_lines_matching: ["^\\s*$", " PASSED", "^\\.+$"]
max_lines: 80
on_empty: "pytest: all passed"
tests: # inline; run via dsl.RunTests (a `verify` command)
pytest:
- name: all-green
input: "pytest\n....\n"
expected: "pytest: all passed"
Ship tests with the filter¶
Inline tests (input → expected) run via dsl.RunTests, so a filter ships with its own regression
check. Above, the all-green case proves that a passing run collapses to the on_empty message —
if a future edit breaks that, the test fails.
Load it into cmdfilter¶
Filters are passed to the cmdfilter component via its filters
config — inline filter YAML docs, added with no recompile:
components:
cmdfilter:
filters:
- |
schema_version: 1
filters:
pytest:
match: "(pytest|=+ test session starts)"
strip_lines_matching: ["^\\s*$", " PASSED", "^\\.+$"]
max_lines: 80
on_empty: "pytest: all passed"
disable_builtins: false # keep the builtin pytest / npm-install / make filters too
cmdfilterisEnabledonly when ≥1 filter is loaded.- It ships builtin
pytest/npm-install/makefilters; setdisable_builtins: trueto run only your own. - The output's first non-empty line is the selector each filter's
matchis tested against, sorted by filter name.
Tip
Filtering that doesn't shrink the output, or output whose first line matches no filter, is a
no-op — cmdfilter leaves the message untouched (fail-open, never-worse). See
cmdfilter.