Feature

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:

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.

Replayable Ze terminal lab

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.

Reproducible
Ze 26.08.311 minute 21 secondsmacOS and LinuxTerminalZe recorder Plain-text transcript
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:

| 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:

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.