Utilities: where / select / to / from - the record-pipe verbs
Status: Built + QEMU-verified (osdev test shell). Four shell built-in pipe stages that
operate on typed records (a Table), not text. They are the user-facing surface of the
structured-pipe subsystem; the design rationale and the model live in docs/records.md. Trails
CLAUDE.md; does not amend it.
sortis documented in29_sort.md- it spans both worlds (line-sort on text, column-sort on records), so it lives with the text filters and is only cross-referenced here.
1. What they are - and why a fresh family
POSIX pipes carry text, so the structure a producer knew (ls knew name/size/type) is
flattened to a string and grep/awk/cut/sed exist to claw it back. GodspeedOS keeps the
structure: a record producer (today status) emits a typed table, and these verbs operate on
real fields. The names say what they do with no POSIX heritage to learn (§ conventions rule 8):
where- keep the rows whose field matches a predicate.select- keep only some columns, in order.to- render the records to a format (json,yaml) for the terminal or export.from- parse text into records (the bridge from the byte world).
They are pipe-only stages - there is no where /file. They appear in a pipeline after a
record producer (status | where …, dir | where …) or after from
(read x.json | from json | where …). The shell-side record producers include status
(task roster, slot/name/core/state/mem/queue/restarts), dir
(name/type/size/sealed), caps (resource/rights), drives
(index/label/status/size_mib/free_mib), find (name/type/path/size),
observe now (the roster plus a ticks cumulative-cpu-time column - observe now | sort reverse ticks is the native "top"), and uptime, events, trace, jobs and wifi list
(is_record_producer and 0_conventions.md rule 12 have the full list). One record producer is
a SERVICE, roster (examples/roster). Only the one-shot observe now is pipeable; the bare live
observe owns the screen and refuses to be piped (a loud error, docs/records.md).
2. Usage
where 0.4.0 - keep records whose field matches
select 0.4.0 - keep only some columns, in order
to 0.4.0 - render records to a format
from 0.4.0 - parse text into records
usage:
<records> | where <col><op><val> ops: = != > < >= <=, and the word `contains`
e.g. status | where mem>0
<records> | select <col> [col…] project the named columns, in order
e.g. status | select name core state
<records> | to <json|yaml|grid> render to JSON / YAML / the plain table at the edge
e.g. status | where mem>0 | to yaml
<text> | from <json> parse text → records (the bridge)
e.g. read /svc.json | from json | where core=1
<verb> version print the version
<verb> help print this message
3. Behaviour
where <col><op><val> - the compact predicate
The predicate is one token, operator attached (no spaces, no quotes unless the value has a
space): where mem>0, where state=BlockRecv, where name!=shell, where core>=1. The parser
finds the operator inside the token by longest match (!= >= <= before = > <):
before it is the column, after is the value. Ops: = != > < >= <= contains (the spaced word; ~ is NOT accepted - rule 3 rejects synonyms). The
comparison is numeric when both sides parse as numbers, else textual. An unknown column
prints where: no such column '<col>', and the rows pass on unfiltered.
select <col> [col…] - projection
Keep only the named columns, in the order given (select name core state reorders too). An
unknown column is a loud error (select: no such column (check the column names)) and stops the
pipeline.
to <json|yaml|grid> - edge renderers
to json emits a JSON array of objects; to yaml emits a YAML list of mappings; to grid emits
the plain aligned table, for a producer that draws something else on the console (trace deps fs | to grid). These live at
the edge of the pipeline - terminal output or, piped to write, export. A record never is
JSON; JSON is one way to print it. With no to, the default rendering is the aligned table grid.
sum / min / max / avg <col> - reduce a numeric column
Four more pipe-only stages collapse a record stream into one bare number: the total, smallest,
largest, or mean of a numeric column (status | sum mem, status | avg queue). The mean is an
integer (truncated). A missing column (sum: no such column 'x') or a non-numeric one (sum: column 'x' is not numeric (never a silent 0)) is loud, and on a byte stream they refuse and
point at from json. Each has its own help and version. One gap: over ZERO rows all four
answer 0 (Table::aggregate), which for min and max is a number no row had.
from <json> - the bridge into the typed world
read gives text; where/select need records. from json parses a flat
[ {…}, … ] array (string / number / true|false / null, no nesting; the first object
defines the columns) into the table model. read deliberately does not auto-parse by extension
- that would be hidden behavior (§26.5);
fromis the explicit, symmetric partner ofto. This crosses the byte ↔ record boundary in the unified pipeline (docs/records.md).
status | where state=BlockRecv | select name core
status | where mem>0 | sort reverse mem | to json
read /svc.json | from json | where core=1 | to yaml | write /c1.yaml
4. Implementation
Pipe stages in the unified dispatcher (pipe_transform): one pipeline threads a
Stream = Bytes(Cap) | Table(Table), and each stage is chosen by its command and by which it
currently holds - so from/to flip between the two worlds and where/select only accept a
Table. A mismatch (e.g. where on raw bytes) is a loud error pointing at the fix. The model is
bounded (§26.6): REC_MAX_COLS (8), REC_MAX_ROWS (64), a fixed REC_ARENA (4096) for
interned strings, REC_COL_NAME (24) - all on the stack, no heap, loud on overflow. No new fs
or kernel surface: these operate on data already in the pipeline.
5. Later (separate so it can grow)
- A JSON string-escaper (values are plain ASCII today);
from yaml; record streams larger than one IPC message (chunkedencode). The SDK record API and the bounded wire codec (encode/decode, emphatically not JSON on the wire,docs/records.md) are done -examples/rosteris a record-producing service (roster | where role=core, nofrom json). - Heterogeneous (differently-shaped) records.
6. Conformance
All four conform to 0_conventions.md: each has its own where/select/to/from help
(usage with a real example per row) and version (number + creator credit), via the shared
help_block helper. Being pipe-only stages, they have no direct-file form; the help makes the
<records> | / <text> | prefix explicit so the grammar is unambiguous.