docs · pre-release
Menu
Day to day / Terminal output

Terminal output

At a terminal, shep draws a box-drawn table that fits your window, colours a sheep's status, restart count, exit code, CPU and memory, and shows status as a face too. Piped into anything else, it prints the same plain columns it always has. You do not have to configure that; it is the default both ways.

What a terminal shows

shep flock in a 120-column window. Every colour here says something a number alone would take a second glance to notice: STATUS carries a face and the word; RESTARTS and EXIT turn a colour once a sheep has actually restarted or crashed; CPU and MEM ramp with how much of each a sheep is using; ID and FOLD are muted throughout, since they never change; and every - is muted too, so an absent value does not compete with a real one:

CPU and MEM have two tiers each, not a gradient. The question they answer is whether one sheep is unusual for this flock, not how much it is using: 160M and 4G both land in the same tier, and on a flock where everything is large the whole column is one colour and tells you nothing. A third tier would have to be the fault colour, which would paint a healthy 4G service as though it had broken. Use --format json when the exact number is the thing you need.

shep flock
host  cpu  ██░░░░░░░░ 15%   mem  ███████░░░ 35.8G / 48.0G   disk  r 32.0K/s w 192.5M/s   net  rx 10.5K/s tx 1.2K/s

┌────┬──────┬───────────────┬───────┬──────────┬──────┬─────┬──────┬──────┬────────┬─────────┬──────┐
│ ID │ NAME │ STATUS        │ PID   │ RESTARTS │ EXIT │ CFG │ CPU  │ MEM  │ UPTIME │ FOLD    │ SMIT │
├────┼──────┼───────────────┼───────┼──────────┼──────┼─────┼──────┼──────┼────────┼─────────┼──────┤
│ 1  │ api  │ (x.x) errored │ -     │ 15       │ 1    │ -   │ -    │ -    │ 0s     │ -       │ -    │
│ 0  │ web  │ (o.o) online  │ 29678 │ 0        │ -    │ -   │ 0.6% │ 3.0M │ 1m 9s  │ backend │ -    │
└────┴──────┴───────────────┴───────┴──────────┴──────┴─────┴──────┴──────┴────────┴─────────┴──────┘

The line above the table is the machine. See the host line.

SMIT is a marker a dog attaches to a sheep over the client protocol — - for every sheep here because none carries one. See terminology.

The faces

Five faces across six statuses, because two of them mean the same thing to someone glancing at a table:

(o.o) grazingonline
(o~o) wakingstarting
(>_<) flinchingwaiting to restart
(-.-) asleepstopping, stopped
(x.x) startlederrored
(?_?) puzzledsilent, dogs only

silent is not a lifecycle status and no sheep ever wears it. See the dogs table.

They are ASCII rather than emoji on purpose. An emoji is double-width, inconsistently so across terminals, and cannot take a foreground colour, which would turn every column measurement into guesswork.

The EXIT column

A sheep that is not running shows why it stopped: an exit code, or a signal name. A running one shows -, the same convention PID and CPU use for a value that has no honest answer yet.

It tells two failures apart that status alone cannot. errored with fifteen restarts says only that a sheep is not up. EXIT 1 says it started and crashed; an empty EXIT on that same row says it never started at all.

The CFG column

A sheep with config waiting on it, or with settings of its own, is marked here; every other sheep reads -. !2 means two fields are parked for this sheep's next spawn, left there by a Flockfile load that would have had to kill the process to apply them, and shep reload <name> promotes them.

A count and not the names, because the names are what shep describe lists underneath its table and a three-character cell has room for one number. See Overrides.

The cell has a second marker, *2, for fields an operator has taken ownership of, whether or not the sheep's current Flockfile declares them. The door that writes them is the config pane: e on a sheep in shep lookout, behind --allow-control. Pending takes the cell when a sheep has both: a parked field is one command away from taking effect, while an override has been true since the last load.

Multi-instance apps group

An app with instances above 1 gets a header row above its slots, in full or plain: web ×3 in NAME, and a ↳ :0 row beneath it per instance, which is also what web:0 selects. A single-instance app is unchanged, no group row and no suffix:

shep flock
┌────┬────────┬──────────────┬───────┬──────────┬──────┬──────┬──────┬────────┬──────┬──────┐
│ ID │ NAME   │ STATUS       │ PID   │ RESTARTS │ EXIT │ CFG │ CPU  │ MEM  │ UPTIME │ FOLD │ SMIT │
├────┼────────┼──────────────┼───────┼──────────┼──────┼─────┼──────┼──────┼────────┼──────┼──────┤
│    │ web ×3 │ (o.o) online │       │ 0        │      │     │ 0.0% │ 9.1M │ 33s    │ -    │ -    │
│ 0  │  ↳ :0  │ (o.o) online │ 74802 │ 0        │ -    │ -   │ 0.0% │ 3.0M │ 33s    │      │      │
│ 1  │  ↳ :1  │ (o.o) online │ 74803 │ 0        │ -    │ -   │ 0.0% │ 3.0M │ 33s    │      │      │
│ 2  │  ↳ :2  │ (o.o) online │ 74804 │ 0        │ -    │ -   │ 0.0% │ 3.0M │ 33s    │      │      │
└────┴────────┴──────────────┴───────┴──────────┴──────┴─────┴──────┴──────┴────────┴──────┴──────┘

The group row rolls up rather than picking a winner: STATUS is the shared word when every instance agrees, else a count per state like 2 up, 1 down. RESTARTS, CPU and MEM sum across the instances, and UPTIME is the minimum, time since the last disruption. ID and PID are blank, since neither has a single answer, and FOLD and SMIT sit only on the group row and stay blank on the instance rows beneath it, since both are per-app already. When a selector or filter matches only some of an app's instances, the count and rollups describe the rows actually listed, not the app's true size.

bare and --format json stay one line per instance instead, so they stay greppable. bare suffixes the name, web:2, and only for an app with more than one instance:

shep flock --style bare
ID  NAME   STATUS  PID    RESTARTS  EXIT  CFG  CPU   MEM   UPTIME  FOLD  SMIT
0   web:0  online  74802  0         -     -    0.0%  3.0M  38s     -     -
1   web:1  online  74803  0         -     -    0.0%  3.0M  38s     -     -
2   web:2  online  74804  0         -     -    0.0%  3.0M  38s     -     -

JSON rows carry the slot as its own field, "instance": 0, rather than folding it into the name.

The dogs table

Dogs get their own table, under a Dogs caption, wherever one appears beside a flock listing. Every column the two tables share sits in the same place, and each table's own columns come last:

bothID NAME STATUS PID RESTARTS EXIT CPU MEM UPTIME
sheep onlyCFG FOLD SMIT
dogs onlySOURCE
shep flock
┌────┬────────┬──────────────┬───────┬──────────┬──────┬─────┬──────┬────────┬────────┬──────┐
│ ID │ NAME   │ STATUS       │ PID   │ RESTARTS │ EXIT │ CFG │ CPU │ MEM  │ UPTIME │ FOLD   │ SMIT │
├────┼────────┼──────────────┼───────┼──────────┼──────┼─────┼─────┼──────┼────────┼────────┼──────┤
│ 0  │ golbat │ (o.o) online │ 48182 │ 0        │ -    │ -   │ -   │ 3.0M │ 2s     │ backed │ -    │
└────┴────────┴──────────────┴───────┴──────────┴──────┴─────┴─────┴──────┴────────┴────────┴──────┘

Dogs
┌────┬────────────┬──────────────┬───────┬──────────┬──────┬─────┬──────┬────────┬─────────┐
│ ID │ NAME       │ STATUS       │ PID   │ RESTARTS │ EXIT │ CPU │ MEM  │ UPTIME │ SOURCE  │
├────┼────────────┼──────────────┼───────┼──────────┼──────┼─────┼──────┼────────┼─────────┤
│ 1  │ log-rotate │ (o.o) online │ 48207 │ 0        │ -    │ -   │ 6.6M │ 2s     │ adopted │
└────┴────────────┴──────────────┴───────┴──────────┴──────┴─────┴──────┴────────┴─────────┘

CFG, FOLD and SMIT are missing from the dogs table because they are impossible for a dog rather than merely empty: a dog belongs to no fold, a smit is a mark a dog paints on a sheep, and no Flockfile load ever reaches a dog, so nothing can park config on one. A column that reads - on every row teaches nothing.

Colours match too, column for column. Same face and status colour, same restart colour above zero, same ramps for CPU and MEM, same EXIT. So the same dog reads the same way under shep dogs, under shep flock, and after shep restart log-rotate.

A dog that never answers reads silent

A dog is a peer as well as a process. It dials back to the shepherd and keeps a connection open, and one that never manages it is not doing its job however alive it looks. So online becomes silent, in amber, with a puzzled face instead of a grazing one.

What to do about it does not fit in a cell, so the table points at where it lives. One line under the dogs table names every silent dog, and shep describe <dog> carries the long form.

What that adds is whether the shepherd has given up. A dog silent past five seconds is restarted once from the binary on disk; if the restarted dog stays silent, the shepherd stops and never restarts it again. Nothing on the row changes when that happens — it still reads silent, because the process is still up and still has not answered — so shep describe is the only place that says so.

Both of them send you to shep bleats <dog>, and that is deliberate. The shepherd writes what it actually saw into the dog's own log at the moment it gives up, and the fix follows from that. A version skew is one cause: cargo install replaced the binary on disk and the process still running is the old one, so a reinstall and a shep restart <dog> fixes it. A dog that reaches the shepherd on every connection and never names itself in its handshake is another, and reinstalling the same build does nothing at all for that one.

Only online is replaced. starting already says the connection is not up yet, and a dog is silent for a moment every time it starts. --format json carries both facts as handshook and dog_stale, beside a status that still reads online — true of the process, which is what that field has always described.

SOURCE says whose code is running

It is the only column in shep carrying a trust distinction, so the two values do not look alike. built-in is shep running its own code, and is muted like any other label you read past. adopted is a third-party binary running at the shepherd's own trust level, from a path you supplied, with no sandbox around it. That gets the same colour a restart count above zero gets: worth a glance, not a fault, because you chose it.

enable, disable, adopt and rehome print a one-row confirmation with the same treatment. Their STATUS holds either a status or a sentence saying why no shepherd answered, and only a status is coloured; a sentence is left plain, because a colour repeating the words beside it is decoration rather than information.

Where else colour appears

Every table shep prints follows one rule: a colour carries information, or the column does not get one. Beyond the flock and the dogs:

trigger, signal, whisperOUTCOME by what happened: it worked, nothing to report, a config gap you can close, or it failed
flush, startupRESULT the same way. absent is muted, not marked: it is the state the verb was asked to produce
barksSINKS, so an alert that failed to deliver does not read like one that arrived
killSOCKET_REMOVED, because a false is what the next boot has to contend with
importREUSE_PORT when it is true, which is work you have to do
note
Seven tables carry no colour at all, deliberately: describe's lamb tree (bar the muted - of an unlabelled lamb), delete's id list, unset's count, the two saved-roll tables, the KV store, and shep dogs --available. Each is either pure data shep has no opinion about, or a single column that is also the whole content, where muting would fade the table and distinguish nothing.

The roll listing is the one table with a STATUS column and no colour on it. Everything in a saved roll is stopped by definition, so the cell reads stopped on every row of every rendering, and a colour that never varies distinguishes nothing.

After a lifecycle command

start, stop, restart, reload, delete and stock all print the whole flock afterwards, not only the sheep they touched. The question after starting one app is usually what everything else is doing, and the exit code already said whether the one you named worked.

shep restart golbat
┌────┬────────┬─────────┬───────┬──────────┬─────────┬─────┬────────┬────────┬────────┬──────┐
│ ID │ NAME   │ STATUS  │ PID   │ RESTARTS │ EXIT    │ CFG │ CPU │ MEM    │ UPTIME │ FOLD   │ SMIT │
├────┼────────┼─────────┼───────┼──────────┼─────────┼─────┼─────┼────────┼────────┼────────┼──────┤
│ 4  │ golbat │ online  │ 14459 │ 5        │ -       │ -   │ -   │ 416.0K │ 0s     │ backed │ -    │
│ 5  │ koji   │ stopped │ -     │ 2        │ SIGTERM │ -   │ -   │ -      │ 0s     │ backed │ -    │
└────┴────────┴─────────┴───────┴──────────┴─────────┴─────┴─────┴────────┴────────┴────────┴──────┘

Dogs
┌────┬────────────┬────────┬───────┬──────────┬──────┬──────┬──────┬────────┬─────────┐
│ ID │ NAME       │ STATUS │ PID   │ RESTARTS │ EXIT │ CPU  │ MEM  │ UPTIME │ SOURCE  │
├────┼────────────┼────────┼───────┼──────────┼──────┼──────┼──────┼────────┼─────────┤
│ 6  │ log-rotate │ online │ 14110 │ 1        │ -    │ 0.0% │ 6.6M │ 41s    │ adopted │
└────┴────────────┴────────┴───────┴──────────┴──────┴──────┴──────┴────────┴─────────┘

That is the same output shep flock gives, dogs table and all, so shep restart log-rotate and shep flock draw the same dog the same way. See the dogs table for why it is a second table rather than another row.

delete is the one verb whose listing cannot show what it did, since the rows are gone. It names the ids it removed on stderr and prints what is left on stdout.

shep delete koji
notice[delete]: deleted 1 sheep, id 5
note
--format json is not widened. A script that runs shep stop web --format json asked about web, so data holds the rows for web. Use shep flock --format json when you want the flock.

The host line

The line above the table is the machine the flock runs on: CPU across every core, memory in use against memory installed, and what the disks and the network moved over the last second. Loopback is left out of the network figures, since a sheep answering a local proxy would otherwise count every request twice.

note
Three of those four are rates, and a rate needs two readings separated by time. The shepherd reads the counters every second and keeps the difference, so a listing takes a number that already exists rather than stopping to measure one. One that booted a moment ago has nothing to subtract from yet, and each rate reads -. Memory is not a rate. - means not measured, never zero.
shep flock, against a shepherd one second old
host  cpu  ░░░░░░░░░░ -   mem  ███████░░░ 35.9G / 48.0G   disk  r - w -   net  rx - tx -

shep lookout draws the memory bar from the same figures. A shepherd older than this feature prints no host line at all, and upgrading the binary is not enough on its own: run shep daemon reload.

Row order

Every listing shep prints is sorted by name, then by id. That covers shep flock, every lifecycle verb above, shep lookout, and the tables signal, whisper and trigger print.

Ids are handed out in registration order, which is a fact about your shell history rather than about the flock, and they move: a delete all followed by a fresh start renumbers everything. Names do not. The id breaks ties, so the four instances of an app stocked to four stay in a fixed order instead of shuffling between refreshes, and it is still what you type at shep stop 11.

Narrow windows

The table measures your terminal and drops columns until it fits, least useful first, and names what it hid. The same flock at 60 columns:

shep flock, 60 columns
host  cpu  ██░░░░░░░░ 15%   mem  ███████░░░ 35.8G / 48.0G  …

┌────┬──────┬────────┬───────┬──────────┬──────┬────────┐
│ ID │ NAME │ STATUS │ PID   │ RESTARTS │ MEM  │ UPTIME │
├────┼──────┼────────┼───────┼──────────┼──────┼────────┤
│ 1  │ api  │ (x.x)  │ -     │ 15       │ -    │ 0s     │
│ 0  │ web  │ (o.o)  │ 68630 │ 0        │ 3.0M │ 18s    │
└────┴──────┴────────┴───────┴──────────┴──────┴────────┘
  CFG, CPU, EXIT, FOLD, SMIT hidden. Widen the window, or use --format json.

Three things narrowed there. The host line is cut from the right and says so with a …: the two bars are what you scan first, so disk and network go before them. Then the status word went before any whole column did, leaving the face, which is five columns rather than fifteen. Then SMIT, FOLD, CFG, EXIT and CPU dropped, in that order, and the footer says so. SMIT goes first: it is by far the widest column, so dropping it buys back the most room.

note
Nothing is ever hidden silently, and --format json always carries every field regardless of how wide your window is. If you are scripting against shep, that is the surface to use.

ID, NAME and STATUS never drop. Below the width those three need, the table prints wider than the window rather than losing the columns that say which sheep a row is about.

What a pipe gets

Redirect it and the boxes, colour and faces all go away. This is not a setting; a non-terminal stdout forces it, and so does --format json:

shep flock | cat
host  cpu  15%   mem  35.8G / 48.0G   disk  r 32.0K/s w 192.5M/s   net  rx 10.5K/s tx 1.2K/s

ID  NAME  STATUS   PID    RESTARTS  EXIT  CFG  CPU   MEM   UPTIME  FOLD     SMIT
1   api   errored  -      15        1     -    -     -     0s      -        -
0   web   online   29678  0         -     -    1.0%  4.1M  1m 9s   backend  -

The host line keeps its numbers and loses both bars, on the same dial that takes the rules off the table. It is not cut either: a pipe has no window, and trimming to the 80 columns shep assumes would lose real figures to a guess.

The columns themselves do move between releases, though. CFG, EXIT and SMIT are all recent, so parse --format json instead if a script depends on the shape.

Errors and notices

An error always goes to stderr. A notice goes to whichever stream its command chose: to stdout when the notice is the answer, as shep style does, and to stderr when it rides beside some other output, so that --format json stays pipeable. Both name their code on the first line and indent everything after it:

shep --home /tmp/no-such flock
error[usage]: no flock at /tmp/no-such
  did you mean to drop --home/$SHEP_HOME? the default is ~/.shep
  to set up a flock there deliberately: mkdir -p '/tmp/no-such'
note
That transcript is a unix one. The layout is identical on Windows and the middle two lines are not. There the knob reads --home/%SHEP_HOME%, the default reads %USERPROFILE%\.shep, and the remedy wraps the path in double quotes after a bare mkdir. Neither cmd.exe nor PowerShell takes -p, and cmd.exe reads a single quote as an ordinary character rather than as quoting.

Only that first line starts at column 0. So a script anchoring error[ or notice[ to the start of a line gets one match per diagnostic, however many lines it runs to. That is the reason for the indent rather than a side effect of it: a path or a filename shep quotes back at you cannot produce a line that reads as shep's own. --format json puts the same text on one line instead, and is still the thing to parse if you have the choice.

Choosing how much

The three levels above the fold are set with shep style:

shep style
$ shep style
notice[style]: full (from the default)

$ shep style plain
notice[style]: wrote plain to /tmp/docdemo/home/shep.toml

$ shep style
notice[style]: plain (from shep.toml)

With no argument it reports the level in force and where it came from, which is what you want when a shell profile and a config file disagree. Four places can decide, and the last one wins:

1. built-in defaultfull
2. shep.toml[style] level
3. $SHEP_STYLEper shell
4. --styleper command
a higher layer says so
$ SHEP_STYLE=full shep style
notice[style]: full (from $SHEP_STYLE)

NO_COLOR

The NO_COLOR convention is honoured at every level, and it is a separate axis rather than a fourth level. It removes colour and leaves the layout alone, so full under NO_COLOR still draws boxes and still shows faces:

NO_COLOR=1 shep flock
│ 1  │ api  │ (x.x) errored │ -     │ 15       │ 1    │ -    │ -    │ 0s     │ -       │

An empty NO_COLOR= counts as unset, per the convention.

Where sheep appear

Beyond the status column, three moments get art, each one a screen with nothing else on it: a flock with nothing in it yet, a flock that is entirely stopped, and shep muster. The count is capped, so mustering forty processes does not paint a field.

note
Never on an error, and never after kill, delete or stop. Errors keep their colour, because red is information, and lose everything else. A sheep beside a failure makes it harder to read, and a cheerful one after deleting a service is simply wrong.

Following the flock

shep flock --follow keeps the table on screen and paints a fresh one over it every second. --interval changes the cadence. One second is the default and the floor.

shep flock --follow, 100 columns
host  cpu  ██░░░░░░░░ 15%   mem  ███████░░░ 35.8G / 48.0G   disk  r 32.0K/s w 192.5M/s   net  rx 10…

┌────┬────────┬────────┬───────┬──────────┬──────┬─────┬──────┬──────┬─────────┬──────┬──────┐
│ ID │ NAME   │ STATUS │ PID   │ RESTARTS │ EXIT │ CFG │ CPU  │ MEM  │ UPTIME  │ FOLD │ SMIT │
├────┼────────┼────────┼───────┼──────────┼──────┼─────┼──────┼──────┼─────────┼──────┼──────┤
│ 0  │ web    │ online │ 26932 │ 0        │ -    │ -   │ 0.0% │ 3.1M │ 17m 39s │ -    │ -    │
│ 1  │ worker │ online │ 27395 │ 0        │ -    │ -   │ 0.1% │ 3.1M │ 17m 31s │ -    │ -    │
└────┴────────┴────────┴───────┴──────────┴──────┴─────┴──────┴──────┴─────────┴──────┴──────┘

The host line is the one a one-shot listing prints, fitted to the window again on every redraw. Changing the interval does not change what it measures: the shepherd still reads once a second, so --interval 5 shows one second of traffic every five seconds rather than a five-second average.

A follow needs a live shepherd and a real terminal. A saved roll is one moment, and a follow is about the moments after it. With stdout redirected it refuses outright rather than writing escape sequences into a file, and --format json gets the same refusal.

A flock taller than the window is cut at the bottom, with a line saying how many it dropped. shep lookout scrolls. This does not.

Following logs

shep bleats prints the tail of each log before it starts following, so a sheep that has already died still tells you why rather than showing you an empty screen while the reason sits in a file you had no particular reason to open.

shep bleats api --lines 2
api | FATAL: port in use
api | FATAL: port in use

--lines says how much, counted per stream, and defaults to fifteen. --lines 0 follows without replaying anything.

A multi-instance app's prefix carries the slot, so lines from two instances stay tellable apart:

shep bleats web
web:0 | listening
web:1 | listening
web:2 | listening

Shared via merge_logs instead, the backlog read from the one file is printed unattributed, web |, since nothing in the file itself says which instance wrote a line. Lines read live after that come from the daemon's own per-sheep events and carry their slot as normal.