Why this matters
Scripts make decisions: "if the backup succeeded, delete the old one; if not, send an alert". The shell's whole decision-making system is built on one small number every command returns when it finishes, its exit status. Understanding it now makes if, loops and error handling in later modules straightforward, and it already lets you chain commands safely.
The exit status
When a process ends, it hands the kernel a number from 0 to 255, and the parent (here, bash) collects it. The convention is the reverse of what many people expect:
- 0 means success. There is only one way to succeed.
- Anything from 1 to 255 means failure. There are many ways to fail, and programs use different numbers to say which. Their man pages list them (for example
grepuses 1 for "no lines matched" and 2 for "an error occurred").
bash stores the status of the last command in the special parameter $?:
$ ls /etc/hostname
/etc/hostname
$ echo $?
0
$ ls /nope
ls: cannot access '/nope': No such file or directory
$ echo $?
2
$ echo $?
0
Why is the last one 0? Because $? now holds the status of the previous echo $?, which succeeded. Read $? right away, or save it: you will learn status=$? in the variables module.
Two builtins exist purely for their status: true does nothing and succeeds, false does nothing and fails.
$ true; echo $?
0
$ false; echo $?
1
Some special values you will see:
| Status | Meaning |
|---|---|
| 0 | success |
| 1 | general failure |
| 2 | misuse (bad option or argument), for many tools |
| 126 | the file was found but is not executable |
| 127 | command not found |
| 128+N | killed by signal N (130 = Ctrl-C, signal 2) |
Try frobnicate; echo $? (127) and sleep 10, press Ctrl-C, then echo $? (130).
Running several commands: ;
A ; (or a newline) separates commands that run one after the other, unconditionally:
$ echo one; false; echo two
one
two
The overall status of a list is the status of the last command run (here echo two, so 0).
Conditional chaining: && and ||
&& means "and then, if that succeeded". || means "or else, if that failed".
$ mkdir data && echo "created data"
created data
$ mkdir data && echo "created data"
mkdir: cannot create directory 'data': File exists
$ mkdir data || echo "could not create data"
mkdir: cannot create directory 'data': File exists
could not create data
The second command after && is skipped when the first fails; after || it is skipped when the first succeeds. These are called short-circuit operators, a name borrowed from logic: in "A and B", if A is false you already know the answer without looking at B.
Real uses:
cd /var/backups && rm old.tar # never rm in the wrong directory if cd fails
make && ./run-tests # only test what actually built
grep -q error app.log || echo "log is clean"
The first line is important. Without &&, a failed cd (typo, missing directory) leaves you where you were, and rm runs there. Many real disasters have exactly that shape.
Combining them
&& and || have the same precedence and group left to right:
$ true && echo yes || echo no
yes
$ false && echo yes || echo no
no
This a && b || c idiom reads like if/else, and is fine when b cannot fail (an echo). But it is not if/else: if a succeeds and b fails, c runs too:
$ true && false || echo "surprise"
surprise
Module 9 gives you a real if that does not have this problem.
Negation: !
Putting ! (followed by a space) before a command inverts its status: 0 becomes 1, anything else becomes 0.
$ ! grep -q root /etc/passwd; echo $?
1
grep -q prints nothing and succeeds if it finds a match; /etc/passwd contains root, so grep succeeds and ! turns that into failure.
Where the status comes from
When a program calls exit(3) (or returns 3 from main in C), the kernel keeps that number until the parent asks for it with a wait system call. bash does the waiting and puts the result in $?. Only 8 bits survive, so exit 256 comes out as 0 and exit 300 as 44. A script returns the status of the last command it ran, or whatever number you give to exit.
Common mistakes
- Treating 0 as false. In many programming languages 0 is false; in the shell, 0 is success, which
&&andiftreat as "true". - Reading
$?too late. Any command in between, evenecho, replaces it. - Using
a && b || cas if/else whenbcan fail.cthen runs afterbfails. - Chaining with
;when the second step depends on the first.cd build; rm -r *is a classic way to delete the wrong files. Use&&. - Assuming "no output" means failure.
grep -qandmkdirprint nothing on success. Check the status, not the screen.
Recap
- Every command ends with an exit status: 0 is success, 1-255 are failures.
$?holds the status of the most recent command; read it immediately.;runs commands in sequence;&&runs the next only on success;||only on failure.a && b || cis a convenient but imperfect if/else.!inverts a status;trueandfalseexist for their status alone.
Going further: run help exit and help true. Then check what grep returns when the file does not exist versus when it simply finds no match: grep x /nope; echo $? and grep zzz /etc/hostname; echo $?.