Output Formatting
A command that answers with DATA sends that answer through the pipe pipeline, whether you're poking around interactively or scripting against ze. One operator set, three ways to use it: set a persistent default, pipe it inline, or apply it offline to already-captured output.
Which operators a given command owes is a property of that command, not of the
language: ze help command --json publishes the list per command, and an
operator the command's answer cannot support is refused by name rather than
answered wrongly. A command that only ever prints text, with no handler on any
surface answering with data, reaches no pipe layer at all and publishes no
operator: show data cat returns the bytes of one stored file, and wrapping
them in a record would corrupt its one use.
The simple way: set a default once
set cli format <name> in the interactive CLI picks a default so every
command already displays that way, with no piping needed at all:
set cli format table
set cli format # shows the current default
| Format | Description |
|---|---|
text |
Space-aligned columns, no box-drawing (default) |
table |
Box-drawing table |
json |
Pretty-printed JSON |
yaml |
YAML output |
ndjson |
One compact JSON object per line |
The choice persists for the session via the ze.cli.format setting; it can
also be set permanently through YANG config.
Piping inline
Append | <operator> to a command that answers with data, shell-like:
show bgp peer list | table
show bgp peer list | json compact
show bgp rib | match established
show bgp peer list | first 5
A chain takes one format operator: json, ndjson, table, text, yaml
or raw. Two together are rejected. Filter and display operators chain freely.
The complete, current set is generated from the operator catalog:
pipe-operators.generated.md. It is the list to
build a tool against, and ./le doc check verify fails when it and the product
disagree. That page carries every operator's name, class, argument, repetition
and description, so this page does not list them again. A second copy is how
five surfaces came to publish five different sets.
Three of its columns are worth reading before you build against it:
- Class says what the operator acts on.
acts on any answeris owed by every command that reaches the pipe layer.acts on rowsis owed only where the answer has rows, and is refused by name where it does not.acts on a stream of updatesmeans something only while a command keeps answering. - Surface says where the operator runs.
saveislocal process only, because a chain the daemon expands would write on the daemon's filesystem. - Repeated says what a second occurrence in one chain means:
applies again, in ordercomposes,no effectis idempotent, andrefusedis refused by name rather than silently answering the last one.
resolve and origin decorate a field the command DECLARES to hold an IP
address, and are refused over a command that declares none.
A display operator changes how an answer is shown. A data operator changes what
the answer holds. The two are independent, so a display mode never suppresses a
data transform: monitor traceroute | log and monitor ping | log render
directly from hop and ping statistics rather than through ApplyPipes, and they
still apply resolve and origin to their legend addresses through the shared
enrichAddr helper.
Scripting against the daemon: | raw
The SSH exec channel is two surfaces at once. An operator running
ssh <host> 'show bgp peer list' gets the format environment cli format default names. A program that parses the answer wants the data behind that
rendering, and it must not change the day an operator changes the default.
| raw is how the second caller asks. It answers the command dispatcher's JSON
byte for byte, so it is the stable contract for a script:
ssh ze-host 'show bgp peer list | raw'
| json is a renderer, not this. It unwraps a single-key object holding an
array, so {"commands": [...]} reaches the caller as a bare [...].
JSON numbers retain their exact value through pipe decoding, row selection,
metadata injection and address enrichment. json and ndjson emit integer
fields as numbers, including 9007199254740993 (2^53 + 1) and
18446744073709551615 (the largest uint64), without float64 rounding or quotes.
The same integers render in full decimal form in yaml, table and text.
This guarantee applies to inline pipes, streamed records and ze pipe stdin;
it cannot recover precision already lost by an upstream producer.
Formatting still unwraps single-key array wrappers where documented. Malformed JSON or trailing content is never accepted as one complete JSON value: format operators pass it through, while line-oriented fallbacks keep their existing behaviour.
Ze's own tooling uses the same operator. Completion, the runtime command tree and the live dashboard each parse an exec-channel answer. Each asks for it through one helper, rather than composing the pipe itself.
The offline way: ze pipe
Scripts and pipelines outside an interactive session apply the same
operators to any captured JSON via ze pipe:
ze show host cpu | ze pipe table
ze show bgp peer list | ze pipe match established
ze show bgp peer list | ze pipe count
ze show bgp peer list | ze pipe yaml
ze show bgp peer list | ze pipe first 5
ze pipe reads stdin (up to 256 MB), applies the pipe chain given as
arguments, and writes the result to stdout. It reads the same catalog. | log
is refused there, by name, because it needs a command that keeps answering;
| no-more is accepted and does nothing, because paging belongs to a live
session. Standalone input carries no declaration, so | resolve and | origin
walk every field whose value parses as an address rather than the declared ones.
ze pipe help lists every operator, split into the three classes below.
Asking the product: ze pipe help --json
Do not hand-copy the operator list into a tool. ze pipe help --json answers
the whole language, and it is generated from the same table the parser reads,
so it cannot fall behind:
ze pipe help --json
Each entry carries the operator's name, its class, the shapes of answer
it acts on, whether it takes an arg, what a second occurrence in one chain
means (repeat), and a one-line description.
The three classes are the contract:
| Class | Meaning |
|---|---|
global |
acts on the answer whatever it holds. Every command that reaches the pipe layer owes these |
data |
acts on rows or fields, so a command owes it only where its answer has them |
stream |
acts on a SEQUENCE of answers, so it means something only where a command keeps answering. log is the one operator in it, and a one-shot command refuses it by name |
shapes names the answer shapes the operator applies to, using the same words
the answer head uses on the wire: doc for one document or one value, map
for rows that describe themselves, tab for rows read against declared column
names.
Saving an answer: | save <path>
| save writes the answer to a file and passes it through, so the terminal
still shows it:
show bgp | json | save /tmp/peers.json
It writes the answer you are looking at, wherever | save sits in the chain.
A chain that names no format has the configured default appended to its end, so
a save applied in place would write the dispatcher's JSON rather than what the
terminal showed.
The file is created readable by its owner alone (0600), because an answer can
carry peer addresses, keys and topology. The write is atomic: a failure leaves
the destination as it was rather than truncated.
| save works where your own process runs the chain: an interactive
session, the monitors, and ze pipe. An interactive session is the one surface
a shell redirect cannot reach, because the answer is drawn to a terminal and
never reaches a pipe.
It is refused over ze cli -c, an SSH exec channel and the web, by name.
The daemon expands the chain on those surfaces, so the file would be written on
the daemon's filesystem, with the daemon's privileges, at a path the caller
chose. Redirect the output from your shell there; that already works.
Configuration presentation
Configuration uses the same separation between data and presentation. Operators
can inspect compact hierarchical blocks, while automation can consume one complete
set path per line:
ze config show router.conf bgp peer transit-a
ze config migrate format set router.conf
ze config migrate format hierarchical converts set syntax back to blocks.
Rendering both forms back to canonical set syntax provides a presentation-neutral
comparison.
Render one configuration for humans and automation
Show one BGP peer as hierarchical blocks and set commands, round-trip between both with identical canonical output, then compose match and count over Ze's plugin registry.
Read the demonstration transcript
$ ze config show router.conf bgp peer transit-a
connection {
local ip 192.0.2.1
remote ip 192.0.2.2
}
session {
asn { local 65000; remote 65001; }
family ipv4/unicast { prefix maximum 1000000; }
}
$ ze config migrate format set router.conf 2>/dev/null | ze pipe match 'bgp peer transit-a'
set bgp peer transit-a connection local ip 192.0.2.1
set bgp peer transit-a connection remote ip 192.0.2.2
set bgp peer transit-a session asn local 65000
set bgp peer transit-a session asn remote 65001
...
$ cmp -s router.set roundtrip.set && echo 'canonical output: identical'
canonical output: identical
$ ze show plugins | ze pipe match flowspec
bgp-nlri-flowspec
flowspec-firewall
...
$ ze show plugins | ze pipe match flowspec | ze pipe count
{"count":3,"pipe":{"count":true}}
Hierarchical and set syntax are alternate presentations of the same parsed configuration. Converting to set syntax and back produces identical canonical set commands. The standalone formatter composes the same match and count operators for shell pipelines.
Column order
| table and | text put a command's columns in the order the command
declares, and every column it does not name after those, alphabetically. A
command that declares nothing renders every column alphabetically, as before.
show bgp declares the order an operator reads a peer in:
address name description remote-as peer-type
state uptime state-changed last-error
routes-received routes-accepted routes-sent
updates-received updates-sent keepalives-received keepalives-sent
eor-received eor-sent connections-dropped
The columns come in the order you read a peer in:
- which peer the row is about
- whether the session is up, and why it last went down
- what the session carries
- the counters you reach for only when something is wrong
| json, | ndjson and | yaml keep their alphabetical keys. A program reads
those three, and key order carries no meaning for a program.
A column is never hidden. Ordering decides where a key renders, never whether it renders. A field you do not see in the order the command declared is still in the table, after the declared ones.
Commands under show bgp declare an order, and so do some commands under
show config, show data, show env, show schema and show yang. Two
channels write them: Go compiled into the daemon, and a plugin's startup
message. Each declaration is an operator judgment about what leads, so a command
takes one as somebody makes that judgment. ze help command "<path>" --json
answers what one command declares, and it reads the two channels together.
Choosing the columns: | display and | fill
A nineteen-column table answers many questions at once. | display cuts it to
the question you asked:
show bgp peer list | display state name
Those two columns render, in that order, and no other column does. Press Tab
after | display and the CLI offers the field names the command declared.
| fill brings the rest back, behind what you displayed:
show bgp peer list | display state | fill # then the command's own order
show bgp peer list | display state | fill alpha # then by field name
show bgp peer list | fill alpha reverse # every column, reverse by name
| Written | The remaining columns come back |
|---|---|
| nothing | not at all |
fill |
in the order the command declares, and by name when it declares none |
fill alpha |
by field name, whatever the command declares |
fill ... reverse |
in the same way, flipped |
The two operators are independent. | display names the fields that lead, and
| fill says what happens to the ones it did not name. With no | display
every column is a remaining column, so | fill alpha sorts the whole table.
Each takes one type of argument, and that is deliberate: | display takes field
names, | fill takes keywords. A token that is a field name in one position and
a keyword in another is a token you cannot complete and cannot read.
A third way was removed on 2026-08-19. | fill overall ordered the columns by
the width they render at, and that width is known only after every cell of the
whole answer has been rendered, so the first row could not be written until the
last had been read. A streamed answer cannot do that. overall is now refused
by name, and | fill and | fill alpha are unaffected.
| display reaches | json, | ndjson and | yaml. | fill does not.
Which fields to answer with is a question you asked out loud, so a program gets
the answer you asked for. The sequence of JSON keys carries no meaning for a
program, so it stays alphabetical.
show bgp peer list | display state name | json # two fields per peer
A name for a chain: pipe aliases
Some selections are worth a name. show bgp answers its aggregate
fields and its peer rows side by side, and an operator usually wants one half:
show bgp | peers # the peer rows, as a table
show bgp | summary # router-id, local AS, uptime and the peer counts
Each is a name for a | display you would otherwise type in full. | peers is
| display peers, and | summary names the five aggregate fields. Press Tab
after the pipe character and both are offered beside the operators.
An alias is fixed at registration, which keeps it readable:
- It takes no argument.
| peers establishedis refused by name. - It never stands for another alias, so what you see is one substitution.
- It is expanded before the command runs, so
| json,| textand every other format carry it.
A plugin names an alias for its own commands too. The BGP RPKI plugin declares
summary on show bgp rpki, so the counters come back without the cache server
rows:
show bgp rpki # the counters and one row for each cache server
show bgp rpki | summary # the counters alone
command help "show bgp rpki" lists the aliases a command answers to, with the
chain each one stands for.
A plugin's alias is registered in the daemon, and one client does not read it.
ze cli with no command argument runs its own copy of the interactive model and
expands the chain itself. A plugin's alias comes back there as
pipe error: unknown pipe operator: summary, and Tab does not offer it. Use
ze cli -c "show bgp rpki | summary", or the interactive session a plain ssh
client reaches. The aliases built into Ze itself, | summary and | peers on
show bgp, resolve in every client.
Command-specific filters
Some commands extend the generic set with their own filter vocabulary,
folded into the command itself rather than applying to its answer. show bgp rib, for example, adds:
| Filter | Description |
|---|---|
received / advertised |
Select the Adj-RIB-In or Adj-RIB-Out side |
peer <selector> |
Filter by peer |
family <afi/safi> |
Filter by address family |
prefix <pattern> |
Filter by prefix |
path <pattern> |
Filter by AS path |
community <value> |
Filter by standard community |
match <pattern> |
Cross-field structured match |
count |
Count matching routes without serializing rows |
first <n> / last <n> |
Take first or last N routes |
histogram |
Count routes by family and prefix length |
graph |
Render an AS-path topology graph (box-drawing) |
reason |
Explain best-path selection (show bgp rib best only) |
These are parsed and validated the same way as generic pipes, but resolved
into command arguments before the command runs, rather than filtering
output afterward. show bgp rib | peer 10.0.0.1 | count counts only that peer's
routes in the RIB iterator. It does not serialize every route and count the
answer. Both kinds run in the daemon. A generic pipe filters what the command
already produced. A command filter stops it being produced.