o'ailly

Chapter 2 — One Shot, One Truth

Draft status: author draft, gate-checked; human verification pending. Outputs shown are real outputs from the authoring machine.

The number is the message

Every command you will ever run ends by handing the kernel a small integer, and that integer is the most reliable sentence Linux will ever speak to you. The exit status is not decoration on the output; it is the output’s verdict. Text can be translated, reworded between tool versions, reimplemented in another language with different phrasing — chapter 1 closed on exactly such a seam, two implementations of ls describing one missing file in two different sentences with one identical status. The number’s meaning, by contrast, is contract: zero is success, nonzero is failure, and the shell’s own manual pins the semantics. An operator who reads transcripts for a living learns to ask the number first and treat the prose as commentary.

The contract has structure worth knowing precisely, because the structure carries diagnosis. Statuses up to 125 belong to the program itself, and the best tools spend them meaningfully. grep is the canonical example — a trichotomy, not a boolean:

printf "alpha\nbeta\n" | grep -q alpha;         echo "selected:   $?"
printf "alpha\nbeta\n" | grep -q missing;       echo "no match:   $?"
grep -q pattern /no/such/file 2>/dev/null;      echo "error:      $?"
selected:   0
no match:   1
error:      2

Status 1 from grep is not an error. It is a successful search whose answer was no — information, and often the information you wanted, as when you verify that a broken setting is gone from a config file. Status 2 is the actual failure: the search could not be conducted. Collapsing those two into “grep failed” is one of the register’s classic self-inflicted wounds, and it matters doubly under the strict-mode flags discussed below, where an innocent “no” can abort a whole script if you have not decided in advance which answer you expect.

Above the program’s own range, the shell reserves statuses to report on programs it could not run: 126 when the file exists but is not executable, 127 when the command was not found at all. In transcript mode, 127 deserves reflex status — it means your question never reached a tool, so the transcript’s text (if any) describes a shell problem, not a system problem. Beyond those, a command killed by a signal reports 128 plus the signal number: 137 is a SIGKILL (nine), very often the out-of-memory killer’s signature; 141 is a SIGPIPE (thirteen), which this chapter will produce on purpose in a moment; and timeout(1) reports 124 for a command it had to cut off:

timeout 1 sleep 5
echo "status: $?"
status: 124

That listing is also the first safety tool of the book. Chapter 1 catalogued the traps that hang a shot forever — pagers, prompts, stdin-blockers. timeout converts all of them from shot that never returns into status 124 after a bound you chose, and in an environment where a hung command costs a whole turn plus a harness timeout you did not choose, wrapping anything remotely doubtful in timeout is not paranoia but budgeting. The habit generalizes: a well-composed shot has a known worst case — in time (timeout), in volume (head, below), and in consequence (chapter 6) — before it is dispatched.

One more property of the number completes the contract: in a pipeline, there are several numbers, and by default the shell hands you only the last. false | true is a success by default. The PIPESTATUS array and the pipefail option (below) exist to recover the rest. Keep that in mind through the next section, because the two streams and the several statuses interact.

Two streams, two audiences

A process is born holding three file descriptors, and the register’s second discipline is to respect the difference between the two it writes: standard output is for the answer; standard error is for commentary about the attempt — progress, warnings, complaints. The convention is old, near-universal, and precisely what makes one-shot composition possible: because df’s answer and df’s complaints travel different pipes, an awk downstream parses the answer without ever seeing the complaints.

In transcript mode you are usually handed both streams, but how they arrive is your choice, and the choice is worth making deliberately. Merged (2>&1), you get a single chronological story — right for debugging, where the complaint’s position among the output lines is itself evidence. Separated (2>errors.txt, or captured independently by your harness), you get a parseable answer channel and a quarantined commentary channel — right for composition, where a warning printed mid-table must not reach your parser. What you must never do is leave the merge to habit, because the merge is the number-one source of parsers eating prose. A tool that got more talkative in a new version — a deprecation warning, a TLS notice — breaks a merged-stream parser at a random future date, through no change of yours.

The merge syntax carries a famous ordering subtlety that a one-shot operator has no interactive opportunity to debug, so learn it once, here. Redirections apply left to right: cmd > file 2>&1 first points stdout at the file, then points stderr at “wherever stdout points now” — both land in the file. Reversed, cmd 2>&1 > file points stderr at “wherever stdout points now” — the terminal or capture pipe — and only then moves stdout to the file: the streams end up split, the file missing the commentary. Both spellings look plausible; only one says what you probably meant. When you want everything a command emitted, in order, in one place, the idiom is cmd > out.txt 2>&1, and no other arrangement of those tokens is its synonym.

The two-audience rule also governs your own emissions. When your shot is itself a small script — an awk program, a loop — put the answer on stdout and route your own diagnostics to stderr (echo "warning" >&2), because the next operator to build on your shot will parse it exactly as you parse df. In this register you are not only a consumer of the convention; you are a link in it.

Determinism: the same shot must mean the same thing

An interactive human re-runs a flaky command and shrugs. A transcript-mode operator comparing today’s output to yesterday’s needs the differences to be signal, and that requires stripping the environment’s fingerprints from the output. Three fingerprints account for most of the noise.

The first is locale. A surprising amount of “what did the command say” is locale-dependent: sort orders, decimal separators, month names, even which column a tool aligns. The classic demonstration is sort, whose ordering under a language locale interleaves cases and can differ between systems, but under LC_ALL=C is the one ordering every machine on earth agrees on — raw byte order:

printf "banana\nApple\ncherry\n" | LC_ALL=C sort
Apple
banana
cherry

Uppercase letters sort before lowercase in byte order, so Apple leads — a result some language locales would reverse. Neither ordering is wrong; the point is that only one of them is pinned. The GNU sort documentation itself warns that locale collation produces surprising results and recommends LC_ALL=C when byte-stable ordering is wanted. The register’s rule: any shot whose output you will parse, diff, or join against another shot’s output gets LC_ALL=C — usually as a prefix on the one command that needs it, so the pin is visible in the transcript rather than hidden in environment setup you would have to remember happened.

The second fingerprint is time. date with no arguments answers in local time with a localized format — pleasant on a screen, poison in a ledger, because “today” formatted in one machine’s timezone does not join against another’s. The register writes timestamps in UTC, in ISO-8601, always: date -u +%Y-%m-%dT%H:%M:%SZ. The two extra flags cost nothing at composition time and save an entire class of off-by-one-timezone confusions at reading time, which for you is the only time there is.

The third fingerprint is the audience fork itself, and here the craft is to prefer formats that are documented as stable over formats that merely look parseable. The ecosystem’s clearest naming of this idea is git’s: git status is a human display, explicitly subject to change between versions, while git status --porcelain is a wire format the documentation promises to keep stable for scripts. Many tools have such a mode under many spellings — --porcelain, -P on df (POSIX-pinned columns, used in chapter 1’s worked shot), --json on a growing set of system tools (chapter 3 makes heavy use of these). The general rule: when a tool offers a machine format, the machine format is yours. The human display was never a contract, and parsing it means your shot’s meaning can be changed by someone else’s cosmetic commit.

Bounding the shot

Chapter 1 named output volume as the register’s second cost; here is the mechanics of paying it. The blunt instruments are head and tail, and the habit of always capping any command whose output size you cannot predict: an unfamiliar log, a recursive listing, a find over a tree of unknown depth. A cap is not merely about politeness to your own attention — an unbounded dump can push the fact you needed out of a truncated capture buffer, so the cap is what guarantees the relevant part arrives. The refined instruments are the tools’ own bounds — grep -m 1 stops at the first match rather than scanning to the end; journalctl -n 50 --since bounds by count and time at the source; find -maxdepth refuses the abyss before descending into it. Prefer the source-side bound where it exists: head discards output after it was produced, while -m, -n, and --since prevent the work itself.

Capping a pipeline, though, springs one of the register’s best-hidden traps, and you should meet it on your own terms rather than in production. When head has taken its fill it exits, the pipe closes, and the producer still writing into that pipe is killed by SIGPIPE — which, per the 128-plus-signal rule, is status 141:

set -o pipefail
seq 1000000 | head -n 1
echo "pipeline status: $?"
1
pipeline status: 141

The answer — the first line — arrived perfectly. The pipeline’s status says a component died of signal 13, because under pipefail the pipeline reports any component’s failure, and seq, mid-write into a closed pipe, was in fact killed. Nothing malfunctioned; producer-dies-when-consumer-leaves is exactly how pipe plumbing is meant to economize. But an operator running under strict mode (next section) will see the shot fail — and a script will abort — on a pipeline that did its job. The escapes, in order of preference: bound at the source instead of piping to head (sed 1q, grep -m, -n flags) so no producer is left writing; or accept and inspect the status knowing 141-with-good-output is benign in this specific shape; or drop pipefail for that one pipeline. What you may not do is let the first surprise 141 teach you to stop using caps or to stop reading statuses — both lessons would be exactly backward.

Strict mode, and its fine print

The preamble set -euo pipefail appears at the top of most modern shell scripts, and you should know both why it earned that position and where its promises end, because the register leans on it harder than interactive use ever did. -e (errexit) aborts the script when a command fails un-checked; -u (nounset) makes expansion of an unset variable an error instead of a silent empty string; pipefail you have just met. Together they convert a script from “keeps going regardless, damage compounding” to “stops at the first surprise” — and in a mode with no human watching the damage compound, stopping early is the correct default. -u in particular defuses one of the most catastrophic accident shapes in all of shell: rm -rf "$prefix/cache" with prefix unset is, without -u, a cheerful attempt to delete /cache; with -u, it is an aborted script and an error message. Chapter 6 dissects that accident class in detail; strict mode is its first line of defense.

The fine print is that -e is a blunt instrument with documented dull spots, and the register’s operators must know them rather than trust the flag as a talisman. A command’s failure does not trigger errexit when the command sits in a tested position — the condition of an if, the left side of && or || — which is usually what you want (that grep status 1 stays usable) but means a misspelled command in those positions also sails on. Failures inside command substitution in an assignment can escape notice entirely:

```bash no-run set -e result=$(false; echo “kept going”) echo “after: $result”

```output
after: kept going

The false failed; the substitution’s status is that of its last command, the echo; the assignment succeeded; strict mode saw nothing. The craft consequences are two. Inside scripts, check the statuses you actually care about explicitly — x=$(cmd) || exit 1, or test the result’s shape ([ -n "$x" ]) rather than assuming errexit guarded the assignment. And in single composed shots — one pipeline, no state — skip the incantation and read $? yourself; strict mode is a script’s discipline, and a one-liner wears it mostly as costume. -u, by contrast, has no such dull spots and belongs everywhere; its measured failure mode on the authoring machine is loud and immediate:

```bash no-run bash -c ‘set -u; echo “$not_defined”’ 2>&1 echo “child status: $?”

```output
bash: line 1: not_defined: unbound variable
child status: 127

(The precise nonzero number varies with how the shell was invoked; the contract you rely on is nonzero, before the expansion is used — the difference between an aborted shot and a deleted /cache.)

Ask and verify in the same shot

The single most compounding habit in the register is this: a shot that changes the machine carries its own check, and prints the check’s result as its final output. Not because your tools are especially untrustworthy, but because in transcript mode the alternative is to assume — and chapter 1’s finality cost means you will not be present to notice a wrong assumption until something built on it fails. The pattern at its smallest:

cd "$(mktemp -d)"
printf "retries = 5\n" > service.conf
grep -c "^retries = 5$" service.conf
1

The write happened; the read-back proves it; the 1 is the proof, in the transcript, where it now exists as evidence rather than as hope. The pattern scales up through && chains — mkdir -p target && test -d target && echo "target ready" — and, for anything with a service on the other end, through a functional probe rather than a structural one: after changing a config, the verifying read is not “is my line in the file” but “does the service now answer the way the change intended” (chapters 4 and 7 build those probes). The register’s phrasing of the principle: a change without a printed verification is, to every future reader of the transcript including you, a rumor. Chapter 8 grows this habit into the evidence-block convention that closes a whole piece of work; it starts here, one grep -c at a time.

Verification composed into the shot also changes failure into information. When the && chain stops early, the transcript shows exactly which link broke — the mkdir, the test, the probe — with no additional forensic turn spent. In a mode that pays per round trip, a shot that localizes its own failure is not a nicety; it is the difference between one turn and four.

Disarming the environment

Last, the preamble that makes the rest possible. Chapter 1’s trap families — pagers, prompts, editors — are disarmed partly per-command (--no-pager, -y, --batch) and partly, more durably, through the environment variables the tools consult before deciding how to behave. A transcript-mode operator’s session environment should say, in every dialect the common tools understand, no one is watching; do not wait for anyone:

```bash fragment

The non-interactive preamble: set once per session, not per shot.

export PAGER=cat GIT_PAGER=cat SYSTEMD_PAGER=cat # pagers: pass text through export GIT_EDITOR=false # editors: fail fast instead of hanging export DEBIAN_FRONTEND=noninteractive # Debian-family installers: never prompt export LC_ALL=C.UTF-8 TZ=UTC # pin collation, encoding, clock ```

The fragment marking is deliberate: this changes session state, and which variables earn a place depends on the tools your machine actually runs — a systemd-less box needs no SYSTEMD_PAGER; a Fedora box replaces the Debian line. The principle is portable even where the spellings are not. Set the environment so that a forgotten per-command flag degrades into safety (a pager that harmlessly cats, an editor that fails instantly and visibly) rather than into a hang. Defense in both layers — the environment as the net, explicit flags as the practice — because in this register a hang and a catastrophe are nearer neighbors than they ever are at a terminal: both end the turn with the machine’s state unknown to you.

The batch: several questions, one dispatch

The chapter has treated the shot as one command, but the round-trip economics of chapter 1 point at a composition pattern this book’s later chapters use constantly: the batch — several independent reads dispatched as one shot, their answers labeled so the transcript stays parseable. The shell’s ; separator is the whole mechanism; the craft is in the labeling and the independence. Labeled, because six commands’ outputs concatenated without markers force the reader to guess where one answer ends — so each section opens with an echo naming what follows, or each line carries its own prefix (the introduction shot in chapter 3 and the layer sweep in chapter 7 are both this pattern in the field). Independent, because ; runs every command regardless of predecessors’ failures — which is precisely right for a diagnostic sweep, where the third read failing must not cost you the remaining four, and precisely wrong for a sequence with dependencies, which is what && is for. The choice between the two separators is therefore a statement of intent: && says these stand or fall together; ; says these are separate questions sharing a stamp. Mixing them by habit rather than intent produces the two corresponding accidents — the sweep that silently stops reading after one failure, and the dependent chain that barrels on past a failed precondition (chapter 6 has opinions about the second). A last sizing rule keeps batches honest: batch reads freely, but a shot should carry at most one write, so that any failure in the transcript maps to at most one change to reason about — the finality cost, budgeted one commitment at a time.

Reading the transcript back

Composition is half the craft of the single shot; the other half is reading what came back, and reading it in the right order. Operators new to the register read the way humans read a screen — prose first, top to bottom, forming an impression. Operators who have been burned read like this: status, then stderr, then the shape of stdout, and only then its content.

Status first, because it reframes everything after it. The same stdout means different things under status 0 and status 2 — a filtered list that arrives alongside status 2 is a partial list, produced before the failure, and treating it as complete is a quiet corruption of everything downstream. Stderr second, because commentary explains verdicts: a status 1 with Permission denied on stderr is a different investigation from a status 1 with silence. Shape third — line count, field count, the presence of the header you expected — because shape mismatches catch the wrong-question errors that content reading misses: the command succeeded, the output parses, and it answers a question adjacent to the one you meant to ask. A grep that returns nothing and status 1 has answered no matches; but if you meant to search a different file, the answer is truthful and useless, and only checking the shape of the invocation against your intent catches it.

Empty output deserves its own paragraph, because in this register emptiness is the most ambiguous sentence a transcript can contain. An empty result with status 0 from a filter usually means ran, found nothing — but it can also mean the input was empty, which is a different fact entirely. When the distinction matters, split it explicitly in the composition: count the input and the matches separately, so the transcript distinguishes “no hot filesystems among the twelve examined” from “zero filesystems examined” — the second being a broken shot wearing a calm face. Chapter 1’s worked df shot printed an affirmative sentence for its empty case for exactly this reason. The rule generalizes into one of the register’s small signatures: good shots say “none”, never just nothing.

Numbers in a transcript deserve one final habit: distrust of unanchored plausibility. An interactive human who typos df into reporting the wrong mount notices, because the screen sits inside a context of intent. A transcript number — 86, 262510 — carries no such context unless the shot printed it: units, the identifier of the thing measured, the threshold it was judged against. That is why the chapter 1 shot printed /mnt/train 98% used, 25828 MiB free rather than 98. Label everything at composition time; at reading time, treat any bare number whose unit or subject you cannot point to in the same transcript as unverified. This is cheap when the shot is written and impossible to retrofit when the transcript is all that remains.

The four-question routine — what was the status? what did stderr say? does the shape match the question? does the content, labeled, answer it? — takes seconds and is the register’s substitute for the peripheral vision a terminal gave for free. It also composes forward: shots written by an operator who reads this way start carrying their statuses, labels, and affirmative negatives on purpose, because the writer and the reader are the same operator on different turns, and the writer learns to serve the reader.

With the command’s anatomy in hand — status first, streams separated, output pinned and bounded, changes self-verifying, environment disarmed — the next question is what to point it at. A machine’s state is not a screenful of dashboards; it is a filesystem of numbers that were always meant to be read one shot at a time. That reading is chapter 3.

1 / 1