Utility: match - keep the lines that match (the grep-equivalent)
Status: Built + QEMU-verified (osdev test files). A shell built-in FILTER: the direct
form match <pattern> <path>, the pipe form <producer> | match <pattern> (slots into the
multi-stage middle), the except inverse, */? glob (shared with find), and minimal
quoting. Trails CLAUDE.md; does not amend it.
1. What it is - and why not "grep"
match reads lines of text and keeps the ones that match a pattern; it drops the rest. It is
the line filter every other system calls grep - a name that is the fossilised keystrokes
g/re/p from the 1970s ed editor (global regular expression print). The name
describes how it was once implemented, not what it does, and it is opaque to anyone who
doesn't know the history (the same family as awk = its authors' initials and sed = stream
editor). GodspeedOS names utilities for what they do (write not touch, delete not rm),
so the line filter is match: a real English verb, and - as the user noted - the same
sense as Rust's match (test a value against a pattern; the truthiness decides the outcome).
If a line matches, it is kept; if it doesn't, it is dropped. One honest verb covers it.
2. Usage
match 0.4.0 - keep the lines that match a pattern
usage:
<producer> | match <pattern> keep piped lines matching <pattern>
match <pattern> <path> keep lines of <path> that match
match except <pattern> [path] keep the lines that do NOT match
match "<two words>" … quote a multi-word pattern
match version print the version
match help print this message
<pattern> = substring (default) | *? glob (like find)
<path> = [index:]label/path | /abs | rel
3. Agreed behaviour
- Two ways in, like
find. A pipe sink (read /log | match error) and a direct form (match error /log). Both reach the same place; a person uses whichever reads better. This mirrorsfind <name> [path], so it is consistent, not bloat. - Matching reuses
find's model. A plain word is a substring match (match error); a pattern with*/?is an anchored glob (match *.txt). One mental model acrossfindandmatch, no new rules. (Shares thecontains/glob_matchhelpers.) - The inverse is
except, a leading keyword.match except errorkeeps every line that does not match - no crypticgrep -v.exceptleads the pattern because trailing would read wrong ("match error except…?"); same shape aswrite append, and it is the keyword only when a pattern follows (so matching the literal word "except" stays possible). It is the same matcher with the boolean flipped - cheap, so it ships with the first cut. - Whole lines. Like
grep,matchkeeps the whole line in which the pattern appears, not just the matched token.
4. Quoting (folded in with match)
match is the command that forces the quoting question: every argument is one
whitespace-delimited token today, so match hello world is broken (pattern hello, stray
world). The honest fix - not a POSIX reflex - is grouping a multi-word argument:
A token wrapped in a matching pair of
'…'or"…"is one argument; the surrounding pair is stripped. No escapes, no nested quotes, no variable expansion; single and double behave identically.
That gives match "hello world" (the real need) and echo "I am text" → I am text (the
visual bounding the user likes, quotes vanishing as expected), while deliberately stopping
before bash's footguns ('can'\''t', $VAR, [[ ]]). The one honest cost: with no escape
character, emitting a lone literal quote is fiddly - an acceptable trade for a system that
isn't trying to be bash.
5. Implementation
A shell built-in filter: input bytes → matching lines out (match_lines, sharing find's
contains + glob_match). In a pipe it consumes the previous stage's buffer (pipe_transform
dispatches the stage, and a text filter lands in run_filter_builtin); as a built-in it runs
in-process, so
it is not subject to the 4 KiB service-boundary cap and can filter a full 16 KiB stage
buffer. The direct form reads the file itself (gs::fs::Fs::read_into, streaming READ_AT) -
no new fs surface - into a fixed FILTER_READ_MAX (8192-byte) buffer; a larger file is
refused loudly (match: <path> is larger than 8192 bytes - too big to filter in one pass), so
pipe it instead (read <path> | match ...).
match is a FILTER, never a pipe producer: match … /file | … is refused (use read /file | match …). Minimal quoting lives in the shared tokenize/strip_quotes helpers, so it benefits
every command, not just match.
6. Later (separate so it can grow)
match regex <expr> [path]- full regular expressions, gated behind the explicitregexword so a layperson never trips over^$.*; only someone who asks for regex gets it. Default stays friendly (substring + glob). Needs ano_stdregex engine, so it is a clear future opt-in, not the first cut.- Case-insensitive matching (an
ignore-casekeyword), if wanted. count(how many matched) is done, as a pipe stage:... | match error | count(28_count.md).
7. Conformance
Conforms to 0_conventions.md: its own match help (usage with a real example per row), the
match except help subcommand help, and match version (number + creator credit), via the
shared help_block helper.
Conforms to rule 10 (0_conventions.md §1.10) since 2026-10-13 (backlog/fixed/89). It found 2026-10-09
that the fs handle was lent no notice; every fs handle the shell makes is now lent the command's
watch (gs::call::Watch), so the command says [q] quit once it has run 2 s, and q, Q or ESC stops the command
at its next request (not measured on its own by osdev test quit; read, which reads a file the same way, is).