Skip to content
VLSI Mentor

Linux & Bash · Part I · Module 11 · Exit Status and Command Chaining

Every Command Returns a Number

Every command you have run in this course has quietly handed back a small number saying how it went, and you have never looked at one. From here to the end of Part I, that number is what decides whether a chain continues, whether a conditional takes its branch, and whether a regression is reported as passing.

Foundation18 min readLinuxPart IFundamentalsExit StatusBash

Part I · Module 11 · Chapter 11.1 · Every Command Returns a Number

Ten modules of commands, and not one of them appeared to return anything. They did. Every single one handed back a number, the shell kept it, and you have been discarding it.

1. The Number

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ls /tmp > /dev/null
$ echo $?
0

$ ls /nope 2>/dev/null
$ echo $?
1

$? holds the exit status of the command that just finished.

It is a small integer, it is set by every command without exception, and the convention attached to it is the thing to learn:

Two commands exist purely to produce the two outcomes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ true;  echo $?
0
$ false; echo $?
1

They do nothing else. true is how you write "succeed" when a construct needs a command, and false the opposite — both turn up constantly from Chapter 11.3 onward. They are also the shortest proof that Chapter 16.1's if tests a command rather than a boolean: true and false are commands, which is the only reason they can sit in a condition at all.

2. The Value Is Gone Almost Immediately

This catches everybody once, and it is the single most important practical fact in the chapter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ false
$ echo "first read:  $?"
first read:  1
$ echo "second read: $?"
second read: 0

The second reading is echo's own status. $? is overwritten by every command, including the one you used to look at it.

The life of an exit status. A command finishes and hands a small number back to the shell. The shell keeps that number in a special variable until the next command finishes, at which point it is replaced. So the value is available only immediately, and reading it twice gives you the second reading of something else.01A COMMAND FINISHESand hands one small number back02THE SHELL STORES ITzero means it worked; anything else means it did not03YOU MAY READ IT ONCEimmediately, before anything else runs04THE NEXT COMMAND REPLACES ITincluding the command that printed it
Figure 1 — the life of an exit status. A command finishes and hands one small number back to the shell, which keeps it in a single variable. You may read that variable immediately, but the very next command to finish replaces it — including the command you used to print it. The value is not a record you can come back to; it is available for exactly one moment.

Save it if you need it twice:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ false
$ st=$?
$ echo "saved: $st"
saved: 1
$ echo "still: $st"
still: 1

An assignment is the one thing safe to put immediately after the command, because a plain assignment always succeeds and — more importantly — $? is evaluated on the right-hand side before the assignment sets its own status.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./run_sim.sh
status=$?                      # capture first
echo "simulation finished"     # now you may do anything
if [ "$status" -ne 0 ]; then ...

Capture on the very next line, every time. Anything between the command and the capture — a log line, a timestamp, a date — destroys the value you wanted.

3. What the Numbers Mean

Any value from 0 to 255 is allowed. Most are chosen by the program, but a handful are set by the shell itself and are the same everywhere.

StatusMeansMeasured with
0Successtrue
1General failure — the usual catch-allfalse, ls /nope
2Misuse: a bad option or argumentgrep on a missing file
126Found, but not executablea directory, or a file with no x bit
127Command not founda typo, or a PATH problem
128 + NKilled by signal N143 for TERM, 137 for KILL
255Out of range, or a common "general error"exit -1

Three of those are worth recognising instantly, because they tell you where to look:

127 is a lookup failure, not a program failure. The program never ran. It is a typo or a PATH problem — Chapter 10.2, and type -a is the next command to type.

126 means the file was found and could not be run. Usually a missing execute bit (Chapter 5.5), sometimes a bad interpreter line, sometimes you aimed at a directory.

137 means the out-of-memory killer, almost always. Chapter 7.3 explained why; 128 + 9 is SIGKILL, and on a shared machine the usual sender is the kernel.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nosuchcommand 2>/dev/null; echo "not found      -> $?"
not found      -> 127
$ ./some-directory 2>/dev/null; echo "not executable -> $?"
not executable -> 126
$ sleep 60 & P=$!; kill -TERM $P; wait $P 2>/dev/null; echo "killed by TERM -> $?"
killed by TERM -> 143

4. Non-Zero Does Not Always Mean Broken

A trap worth meeting early: several common tools use their status to report a result, not an error.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ grep alpha f1.txt >/dev/null; echo "grep found      -> $?"
grep found      -> 0
$ grep zzz   f1.txt >/dev/null; echo "grep not found  -> $?"
grep not found  -> 1
$ grep alpha nofile 2>/dev/null; echo "grep bad file   -> $?"
grep bad file   -> 2

grep returns 1 when the pattern is simply absent. Nothing went wrong — it looked, and there was nothing there. It reserves 2 for an actual error such as an unreadable file.

diff does the same:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ diff f1.txt f2.txt >/dev/null; echo "diff same    -> $?"
diff same    -> 0
$ diff f1.txt f3.txt >/dev/null; echo "diff differ  -> $?"
diff differ  -> 1
$ diff f1.txt nofile 2>/dev/null; echo "diff bad file -> $?"
diff bad file -> 2

Chapter 4.6 noted this convention; here is where it earns its keep. The three-way split — 0 for one answer, 1 for the other answer, 2 for "I could not tell you" — is a good design, and it is why grep -q is the standard way to ask a yes-or-no question about a file's contents.

5. Where the Status Comes From

For an external program, the status is whatever the program's own code returned — a C program's return from main, a Python script's sys.exit().

For a builtin, the shell sets it directly.

For a shell script, it is set by exit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
if [ ! -f "$1" ]; then
  echo "no such file: $1" >&2
  exit 1
fi
echo "found it"
exit 0

exit with no number returns the status of the last command that ran, which is convenient and occasionally a trap — a script ending in a stray echo reports success regardless of what happened earlier.

A script that simply runs off the end also returns its last command's status. Be explicit when the answer matters.

Assignments and the two forms that hide it

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ v=hello;      echo "plain assignment -> $?"
plain assignment -> 0
$ v=$(false);   echo "v=\$(false)       -> $?"
v=$(false)       -> 1

A plain assignment always succeeds. One containing a command substitution takes that command's status, which is how Chapter 8.4 said to test whether a capture worked.

But put a declaration keyword in front and it is lost:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ declare v=$(false); echo "declare v=\$(false) -> $?"
declare v=$(false) -> 0

And the version that bites hardest, inside a function:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ f() { local v=$(false); echo "  local v=\$(false)    -> $?"; }
$ g() { local v; v=$(false); echo "  local v; v=\$(false) -> $?"; }
$ f; g
  local v=$(false)    -> 0
  local v; v=$(false) -> 1

6. Choosing Your Own

A script that will be used by anything else should mean something by the number it returns.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/bin/bash
# exit codes:
#   0  all tests passed
#   1  one or more tests failed
#   2  bad usage
#   3  the simulator could not be found

results="${1:-}"
[ -n "$results" ] || { echo "usage: $0 <results-file>" >&2; exit 2; }
command -v vcs >/dev/null || { echo "simulator not on PATH" >&2; exit 3; }
...

Three conventions worth adopting:

  • Document them at the top. The list above costs four comment lines and saves the next person reading the whole script.
  • Keep them under 126. Above that you collide with the shell's reserved values and with 128 + signal.
  • Reserve 0 for complete success. "Succeeded with warnings" should be its own non-zero code, not a zero with a message — because the thing reading your status is a machine and it only looks at the number.

This matters most where you cannot see the output: a regression harness, a nightly job, a continuous-integration step. All of them decide pass or fail from exactly one integer.

7. Seeing It Everywhere

Once you know $? exists, the shell's prompt can show it, which is the fastest way to build the habit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# in ~/.bashrc  (Chapter 10.3)
PS1='$(s=$?; [ $s -ne 0 ] && echo "[$s] ")\u@\h:\w\$ '

Now a failing command leaves its status visible in front of the next prompt. Within a week you will notice the 127s and the 126s without thinking about it.

And for inspecting one command without disturbing anything:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
some_command; echo "-> $?"

Crude, immediate, and the thing to type whenever a command's behaviour is unclear.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/ec && cd /tmp/ec
printf 'alpha\nbeta\n' > f1.txt
printf 'alpha\nbeta\n' > f2.txt
printf 'alpha\ngamma\n' > f3.txt

1 — Read the two outcomes.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
true;  echo "true  -> $?"
false; echo "false -> $?"
ls /tmp >/dev/null; echo "ls ok   -> $?"
ls /nope 2>/dev/null; echo "ls bad  -> $?"

2 — Watch the value vanish.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
false
echo "first:  $?"
echo "second: $?"

Then do it properly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
false
st=$?
echo "saved: $st"; echo "still: $st"

3 — Collect the reserved codes.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
nosuchcommand 2>/dev/null;  echo "127? -> $?"
mkdir -p adir; ./adir 2>/dev/null; echo "126? -> $?"
printf 'x\n' > noexec.sh;   ./noexec.sh 2>/dev/null; echo "126? -> $?"
sleep 60 & P=$!; kill -TERM $P; wait $P 2>/dev/null; echo "143? -> $?"

4 — Watch the wrap.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bash -c 'exit 300'; echo "300 -> $?"
bash -c 'exit 256'; echo "256 -> $?"
bash -c 'exit -1';  echo "-1  -> $?"

5 — Meet grep's three answers.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep alpha f1.txt >/dev/null; echo "found     -> $?"
grep zzz   f1.txt >/dev/null; echo "not found -> $?"
grep alpha nofile 2>/dev/null; echo "no file   -> $?"

6 — Make set -e bite, then fix it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf '#!/bin/bash\nset -e\ngrep zzz /tmp/ec/f1.txt\necho "NOT REACHED"\n' > se.sh
chmod +x se.sh; ./se.sh; echo "  exited $?"
printf '#!/bin/bash\nset -e\ngrep zzz /tmp/ec/f1.txt || true\necho "reached"\n' > se2.sh
chmod +x se2.sh; ./se2.sh; echo "  exited $?"

7 — Catch the local masking bug.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
f() { local v=$(false); echo "  local v=\$(false)    -> $?"; }
g() { local v; v=$(false); echo "  local v; v=\$(false) -> $?"; }
f; g

8 — Write a script with documented codes. Three exits — 0, 2 for bad usage, 3 for a missing tool — and check each with echo $?.

9 — Put it in your prompt for a day:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
PS1='$(s=$?; [ $s -ne 0 ] && echo "[$s] ")\u:\w\$ '

10 — Clean up. cd /tmp && rm -rf /tmp/ec

Check Your Understanding

1. Why does zero mean success when every programming language you know uses zero for false?

2. false; echo "status: $?"; echo "again: $?" prints 1 then 0. Why?

3. A command returns 127. What has gone wrong, and what do you check?

4. A script ends with exit $failed_count and reports success after a bad run. How?

5. grep ERROR run.log under set -e stops the script even though the log is clean. Why, and what is the fix?

6. local count=$(wc -l < "$f") never seems to detect a failure. Why not?

8. Remember

  • Every command returns a status, and $? holds the most recent one.
  • Zero means success; anything else is a reason. It is a count of what went wrong, not a boolean.
  • true and false exist to produce the two outcomes and nothing else.
  • $? is overwritten by the very next command, including the echo that printed it. Capture it on the next line.
  • The range is 0 to 255 and it wraps. exit 300 becomes 44. Never return a count.
  • 127 means not found — a typo or PATH. 126 means found but not runnable — usually a missing execute bit.
  • 128 + N means killed by signal N; 137 is almost always the out-of-memory killer.
  • Keep your own codes under 126 and document them at the top of the script.
  • Non-zero does not always mean broken. grep and diff return 1 for "the other answer" and reserve 2 for a real error.
  • That is why set -e trips on a clean grep. || true when you do not care.
  • exit with no number returns the last command's status, so a stray echo at the end reports success.
  • local v=$(cmd) discards the command's status. Declare first, assign second.
  • A machine reads your exit status and nothing else, which is what makes it worth choosing deliberately.
  • Some tools do not keep their half of that bargain. Chapter 22.2 measures a simulator that exits 0 on a failing test, which is why its log has to be read.
  • A tool's exit status is an interface; its message text is not. Chapter 25.7 measures four tools with three different version flags and four different rejection statuses.

You can now read the number. The next chapter is about what happens when you put two commands on one line — and about the fact that the simplest way of doing so ignores this number entirely.

Where this fits

Part of the Linux & Bash curriculum.