I had been carrying around a slightly fuzzy idea of command success.
If a command printed the thing I expected, it had worked.
If it printed an error, it had failed.
That turns out to be a poor mental model for the shell.
A command can print nothing and succeed.
It can print something and fail.
It can even print something that looks like success while deliberately reporting failure.
The shell has another channel for answering the question:
Did this command succeed?
That channel is the command’s exit status.
Zero means success
In Bash, an exit status of 0 represents success. A non-zero status represents failure.
That initially felt backwards to me.
Why isn’t 1 true and 0 false?
The Bash documentation gives a useful reason: there is one general successful outcome, but potentially many different kinds of failure. A single value can therefore represent success while the remaining non-zero values are available to distinguish failure conditions.
The simplest demonstration uses two wonderfully uneventful commands:
```bash id=”fs12pz” true printf ‘status=%s\n’ “$?”
false printf ‘status=%s\n’ “$?”
The result is:
```text id="15zlo1"
status=0
status=1
Neither true nor false needs to produce useful output.
Their entire purpose here is their status.
GNU Coreutils describes true as a command that does nothing successfully and false as one that does nothing unsuccessfully.
Bash also provides true and false as builtins, so at a Bash prompt these examples do not normally require launching the Coreutils executables.
There is something delightfully Unix-like about having two commands whose job is essentially:
```text id=”g2ssv1” true → yep false → nope
## `$?` tells Bash what just happened
Bash makes the exit status of the most recently completed command available through the special parameter:
```bash id="76qci6"
$?
For example:
```bash id=”97skpe” ls /tmp printf ‘%s\n’ “$?”
If `ls` succeeds, I should normally see:
```text id="4fk3ru"
0
Now try something that does not exist:
```bash id=”yvm89p” ls /definitely-not-a-real-directory printf ‘%s\n’ “$?”
`ls` prints an error and returns a non-zero status.
The important part is that these are **two different outputs carrying two different kinds of information**.
Something like:
```text id="kaqjdh"
ls: cannot access '/definitely-not-a-real-directory': No such file or directory
2
contains:
```text id=”6n21a0” diagnostic message │ └── information for a human
exit status │ └── success/failure information for the caller
That separation is starting to feel important.
## Output is not success
I can make the distinction obvious with a shell function:
```bash id="7lwjfr"
confusing_command()
{
printf 'Everything worked perfectly!\n'
return 7
}
Run it:
```bash id=”2up468” confusing_command printf ‘status=%s\n’ “$?”
and I get:
```text id="pqyf5s"
Everything worked perfectly!
status=7
The text says success.
The exit status says failure.
Bash believes the exit status.
I can reverse the trick:
```bash id=”7b19s1” also_confusing() { printf ‘DISASTER! EVERYTHING IS BROKEN!\n’ return 0 }
Then:
```bash id="41xf87"
also_confusing
printf 'status=%s\n' "$?"
produces:
```text id=”nr9f33” DISASTER! EVERYTHING IS BROKEN! status=0
As far as the shell is concerned, the command succeeded.
So:
```text id="rw8kbq"
output ≠ exit status
They may agree.
Well-designed command-line programs generally try to make them agree.
But the shell does not infer success by reading English sentences from the screen.
Thankfully.
That would be a fairly horrifying API.
This is why shell conditionals work
The distinction becomes much more useful when I stop printing $? manually.
Bash can use a command’s exit status directly as a condition:
```bash id=”fnlhji” if grep -q “Linux” notes.txt; then printf ‘Found it\n’ else printf ‘Not found\n’ fi
The `if` statement isn't asking:
> Did `grep` print something?
It is testing the command's exit status.
There is a subtlety hiding in that `else`.
For GNU `grep`, an exit status of `0` means a match was found, `1` means no match was found and `2` normally indicates an error. So the `else` branch really means:
> `grep` did not return success.
It does not necessarily mean:
> The text was not found.
That distinction leads directly to another useful correction.
Commands can participate in shell logic without needing to produce machine-readable words such as:
```text id="pcbjyx"
TRUE
FALSE
YES
NO
SUCCESS
FAILURE
The status itself carries that information.
Conceptually:
```text id=”u5dh5t” command │ ▼ ┌───────────────┐ │ execute work │ └───────┬───────┘ │ exit status ┌─────┴─────┐ │ │ ▼ ▼ 0 non-zero success failure │ │ ▼ ▼ then else
That is much cleaner than parsing whatever happened to appear on standard output.
## Non-zero does not mean one particular error
Another useful correction:
```text id="lrrxza"
non-zero
does not mean:
error number one.
Different commands can use different non-zero statuses to communicate different outcomes.
For example, Bash itself reserves or uses certain statuses for particular execution failures. A command that cannot be found results in status 127, while a command that is found but cannot be executed results in 126.
Individual programs can also define their own exit-status meanings.
So the portable high-level rule is:
```text id=”rc5sjd” 0 → success non-zero → not success
If I care what a particular non-zero value means, I need to read the documentation for that program.
Exit statuses are an interface.
And interfaces need contracts.
## `$?` is fleeting
There is a small trap hiding here.
`$?` contains the status of the **most recent command**.
Consider:
```bash id="26h0rs"
false
printf 'first status=%s\n' "$?"
printf 'second status=%s\n' "$?"
The first printf sees the result of:
```bash id=”t3b80c” false
so it prints:
```text id="vuv89y"
first status=1
But that printf itself succeeds.
By the time the second printf expands $?, the most recently completed command is no longer false.
It is the first printf.
So I get:
```text id=”x7s0m7” second status=0
The history looks like:
```text id="du3fd7"
false
│
└── status 1
│
▼
$?
printf ...
│
└── status 0
│
▼
$?
$? is not a permanent variable containing “the last interesting failure”.
Every subsequent command gets a chance to replace it.
If I need to preserve a status, I should capture it immediately:
```bash id=”p1q0bk” some_command status=$?
printf ‘command returned %s\n’ “$status”
This seems like a tiny Bash detail.
I suspect it will eventually save me from some extremely annoying scripts.
## The caller decides what the status means
Another thing clicked here.
An exit status is a communication channel between a command and whatever invoked it.
That caller might be:
* an interactive shell;
* a Bash script;
* another program;
* a test runner;
* a build system;
* some automation I haven't written yet.
The human-readable output may be completely irrelevant to that caller.
Imagine:
```bash id="264vwj"
if backup_database; then
send_success_notification
else
raise_alert
fi
The automation needs a reliable signal, not a sentence to interpret.
Instead:
```text id=”bsw4mg” backup_database │ ├── stdout/stderr → information │ └── exit status → outcome
That seems like a much stronger interface.
## Output has its own destinations
This also connects to something I noticed while looking around `/proc`.
Processes normally have separate file descriptors for:
```text id="01i7wf"
0 standard input
1 standard output
2 standard error
So even “output” itself isn’t one channel.
By convention, a command often writes ordinary results to standard output and diagnostics to standard error, while separately finishing with an exit status. The file descriptors themselves do not enforce those meanings.
Conceptually:
```text id=”v2qmw1” command │ ┌───────────┼───────────┐ │ │ │ ▼ ▼ ▼ stdout stderr exit status │ │ │ ▼ ▼ ▼ ordinary output diagnostic outcome (usually) output (usually)
Those channels can be used together, but they solve different problems.
## A small experiment
This makes the distinction quite visible:
```bash id="upxb31"
demo()
{
printf 'normal output\n'
printf 'diagnostic output\n' >&2
return 9
}
demo
status=$?
printf 'exit status: %s\n' "$status"
There are three separate pieces of information:
```text id=”pbrbep” normal output diagnostic output exit status: 9
I can redirect one without changing the exit status:
```bash id="jcooz3"
demo >output.txt
status=$?
printf 'exit status: %s\n' "$status"
Or redirect both output streams:
```bash id=”s9o18h” demo >output.txt 2>error.txt status=$?
The command still returns `9`.
Where the output went and whether the command succeeded are independent questions.
That feels like the important idea.
## The mental model I'm keeping
Previously:
```text id="5i58ha"
command prints expected thing
│
▼
success
Now:
text id="lgr3f9"
COMMAND
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
standard output standard error exit status
│ │ │
▼ ▼ ▼
results diagnostics outcome
│
┌──────┴──────┐
│ │
▼ ▼
0 non-zero
success failure
The thing I am taking away is almost embarrassingly simple:
Success is determined by exit status, not by output.
But I can already see why Unix tools are designed this way.
Humans can read output.
Programs can evaluate statuses.
And shell scripts can compose commands according to whether the previous operation actually worked.
There is another complication waiting here: commands connected together in pipelines can produce more than one exit status while Bash still needs to decide what status the pipeline should have.
I have seen an option called pipefail.
I have absolutely no idea what it does yet.
That sounds like a problem for future me.
References and further reading
GNU Bash Reference Manual: Exit Status
Documents Bash’s exit-status model: zero indicates success, non-zero indicates failure, and the status of the most recently completed command is available through $?.
GNU Bash Reference Manual: Special Parameters
Documents $? and the other special shell parameters maintained by Bash.
GNU Bash Reference Manual: Conditional Constructs
Explains how shell constructs such as if make decisions based on command exit statuses.
GNU Coreutils: true and false
Documents the intentionally simple commands used in the experiments above: true succeeds and false fails.