The Shell, the Terminal and the Kernel · about 35 min

Exit status and command lists

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:

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:

StatusMeaning
0success
1general failure
2misuse (bad option or argument), for many tools
126the file was found but is not executable
127command not found
128+Nkilled 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

Recap

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 $?.

Exercises

Exercise 1 · quiz

Predict the output

What does this line print?

false || echo B && echo C

Answer
  • B then C on separate lines

&& and || have equal precedence and are read left to right. false fails, so || echo B runs and prints B; that echo succeeds, so && echo C runs and prints C. Think of it as (false || echo B) && echo C. The pattern a && b || c is often used as a shortcut for if/else, but notice that c also runs when b fails. Real if statements avoid that trap.

Exercise 2 · task in a shell

Capture an exit status

Run ls /nonexistent in the shell. It fails. Find its exit status with $? and save just the number into /work/status.txt.

Careful: $? holds the status of the most recent command, so read it immediately after the failing command. One way is ls /nonexistent; echo $? > status.txt on one line.

Hint 1

After the failing command, echo $? prints its status. Anything you run in between replaces $?.

Hint 2

ls /nonexistent; echo $? > status.txt. The error message itself still appears on the screen; that is fine.

Reference solution
ls /nonexistent; echo $? > status.txt

Exercise 3 · task in a shell

Only if it worked

In the shell, write one command line that

  1. creates the directory /work/reports with mkdir, and
  2. only if that succeeded, creates the file /work/reports/ok.txt containing created,
  3. and if either step failed, writes failed into /work/error.txt.

Run it once (it should create reports/ok.txt). Then run the exact same line a second time: now mkdir fails because the directory exists, and error.txt should appear. Press Check after running it twice.

Hint 1

a && b runs b only if a succeeded; a || b runs b only if a failed.

Hint 2

Combine them: mkdir reports && echo created > reports/ok.txt || echo failed > error.txt

Reference solution
mkdir reports && echo created > reports/ok.txt || echo failed > error.txt
mkdir reports && echo created > reports/ok.txt || echo failed > error.txt

Exercise 4 · write code

A script that reports

Write a script that tries to create a directory named out in the current directory.

The checker runs your script twice in the same directory, so the first run should print made out and the second out already exists. mkdir's own error message goes to the error stream and is not compared, but you may hide it by adding 2>/dev/null after the mkdir command (you will learn why that works in module 5).

The script must also exit with status 0 both times.

File: script.sh

Starting point

# try to create the directory "out" and report what happened
mkdir out

Example

expected output

made out

Example

expected output

out already exists
Hint 1

This is the a && b || c pattern from the lesson.

Hint 2

mkdir out && echo "made out" || echo "out already exists". The status of a script is the status of its last command; here that is an echo, which succeeds.

Reference solution
mkdir out 2>/dev/null && echo "made out" || echo "out already exists"