Skip to content
academia.sh

Lesson 05 / 24

Exit Status

The success/failure convention, reserved code ranges, conditional chaining operators, and why these operators are not confused with conditional branching.

Contents

In the previous lessons, commands’ results always came out as expected. In reality, grep may find no match, cut may fail to open the file, sort may not find enough temporary space. The shell’s way of learning about these situations is a single number.

Every process leaves an integer with the kernel when it terminates. The shell takes this number as the exit status and stores it in the special variable $?. Shell programming’s control flow — conditions, loop conditions, error handling — is built entirely on this number.

Why Zero Is Success

The convention is this: 0 is success, every nonzero value is failure.

In the Programming Fundamentals course, the value true was used for logical correctness; the shell’s convention looks like the reverse of this. The reason is simple: a job succeeding has exactly one meaning, while its failing has many causes. The 255 nonzero values can be used to distinguish different kinds of failure; a single value suffices for success.

grep is a clear example of this distinction, and it separates three codes from each other:

grep -q '404' access.log;      echo "found      -> $?"
grep -q '999' access.log;      echo "not found  -> $?"
grep -q '404' missing-file.log 2>/dev/null; echo "file missing -> $?"
found      -> 0
not found  -> 1
file missing -> 2

All three differ from each other, and this difference does real work. “No match” is an expected result; “file missing” is a situation where the script should stop. A script that looks only at “zero or not” confuses the two.

The same rigor is expected in your own scripts: give different codes to different error classes, do not use code 1 for everything.

Commands That Embody the Convention

The true and false builtins do no work at all; they only return a code.

true;  echo "true  -> $?"
false; echo "false -> $?"
true  -> 0
false -> 1

The : command is equivalent to true, and it was used in the previous lesson to empty a file.

The test command, and its bracket notation [ ... ], tests a condition and returns the result as a code. This command has no output at all; all the information is in the code.

[ -f access.log ]; echo "file exists  -> $?"
[ -f missing.log ];    echo "file missing -> $?"
file exists  -> 0
file missing -> 1

[ is not a syntax element but an executable command name; this is why the spaces around it are mandatory, and the closing ] is its last argument. The detail of tests will be covered in the Conditions and Tests lesson.

The Code’s Numeric Limits

An exit status fits into a byte, in the range 0–255. Values outside this range are reduced modulo 256:

(exit 300); echo "300 -> $?"
(exit -1);  echo "-1  -> $?"
300 -> 44
-1  -> 255

300mod256=44300 \bmod 256 = 44 and 1mod256=255-1 \bmod 256 = 255. Returning a code greater than 255 in your script has no point; the number silently turns into something else.

Reserved Codes

The shell reserves some codes for its own use. Giving these codes to your own error classes leads your script to mislead its caller.

nosuchcommand 2>/dev/null; echo "command not found -> $?"
./unrunnable.txt   2>/dev/null; echo "not executable    -> $?"
command not found -> 127
not executable     -> 126

127 reports that the command was not found in the search path; 126 that it was found but could not be run (no execute permission, or it is a directory). If you get 127 when calling a script, the problem is not inside the script but in its name or the search path.

For processes that terminate by a signal, the shell produces a code in the form 128 + signal number:

bash -c 'trap - INT; kill -INT $$'; echo "INT  -> $?"
bash -c 'kill -TERM $$';            echo "TERM -> $?"
INT  -> 130
TERM -> 143

SIGINT is number 2 (128+2=130128 + 2 = 130), SIGTERM is 15 (128+15=143128 + 15 = 143). This is why a script interrupted from the keyboard returns 130. Signals and catching them are covered in detail in the Writing Robust Scripts topic.

The usable range in practice is 1–125. If the exit command is called with no argument, it returns the code of the last command that ran in the script.

Conditional Chaining

The shell defines two chaining operators that depend on the exit status.

The && operator runs the right-hand command if the left-hand one returns 0. The || operator runs the right-hand command if the left-hand one returns nonzero. In both cases, if the right-hand command does not run, the chain’s code is the left-hand one’s code.

grep -q '404' access.log && echo "log has 404"
grep -q '999' access.log || echo "999 does not appear"
log has 404
999 does not appear

These operators perform short-circuit evaluation — the same behavior defined for logical operators in the Programming Fundamentals course. The difference is that what is being evaluated here is not an expression but a process being run: short-circuiting does not just save time, it ensures a job is never done at all.

The safe in-place transformation pattern from the previous lesson was written with this operator:

sort small.txt > small.new && mv small.new small.txt

If sort fails, mv never runs, and the original file stays in place. Tying a destructive operation to a success condition is the plainest form of error handling in the shell.

Chaining Is Not Conditional Branching

&& and || have the same precedence and associate left to right. These two rules give rise to a common misunderstanding: the notation condition && body || fallback is thought to be equivalent to ifelse. It is not.

fail() { echo "body ran"; return 1; }

true && fail || echo "FALLBACK RAN"
body ran
FALLBACK RAN

The condition was true, the body ran — but because the body reported failure, the fallback ran too. ifelse does not behave this way:

if true; then fail; else echo "FALLBACK"; fi
body ran

Rule: an &&|| chain substitutes for ifelse only when the body can never fail. Because this guarantee can rarely be given, writing if for a two-branch choice is the correct default. && and || are suited to single-branch guards — “continue if it succeeded,” “warn and exit if it failed.”

Left-to-right associativity also shows itself in mixed chains:

false || echo "first" && echo "second"
first
second

false || echo "first" is evaluated first; because echo succeeds, the chain’s code is 0, and && runs the right-hand side. Use grouping instead of hard-to-read chains: the code of a command list inside { ...; } is the code of the list’s last command.

Negation

The ! prefix reverses a command’s code: it turns 0 into nonzero, and nonzero into 0.

! grep -q '999' access.log; echo "negation -> $?"
negation -> 0

This is the direct way to express the condition “if 999 does not appear in the log.” Negation has a second function too: suppressing expected failures under strict mode (set -e). This use is covered in the Error Handling lesson.

Wrapping Up the Topic

The four concepts established in this topic form the vocabulary for the rest of the course. The pipeline that extracts the five most requested paths from the log uses this entire vocabulary:

[ -r access.log ] || { echo 'log unreadable' >&2; exit 2; }
cut -d' ' -f7 access.log | sort | uniq -c | sort -rn | head -5

The first line contains an exit status test, a chaining operator, a grouping, and a redirection to standard error. The second line is a four-command pipeline. Together, the two form the core of the report.sh script to be written in the next topic.

Summary

  • Every process leaves an exit status in the range 0–255 when it terminates; 0 is success, every nonzero value is failure. The code is read with $?.
  • Different kinds of failure are distinguished by different codes; grep returns 1 for no match, 2 for a file error.
  • 126, 127, and 128 + signal number are reserved by the shell; the range 1–125 is used for your own codes.
  • && and || perform short-circuit evaluation; they have the same precedence and associate left to right.
  • The notation condition && body || fallback is not equivalent to ifelse when the body can fail.

Next Step

Everything in this topic was done with single-line commands. When the same pipeline needs to run every day, gain options, and be handed off to someone else, a single line is not enough. The next topic builds the script file: it opens by covering what the interpreter declaration does, how a script becomes executable, and which shell is used when the system runs a file.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close