Module 9 · Shell scripting basics
Exit codes, && and ||
Every command reports success or failure in a number; learn to read it, set it from your own scripts, and chain commands so one only runs if the previous worked, or failed.
What you will learn
- Read `$?` and know the conventional meanings of 0, 1, 2, 126 and 127.
- End a script with a chosen status using `exit N`.
- Chain commands with `&&` and `||`, and use `set -e` for fail-fast scripts.
When a command finishes it hands the shell a number from 0 to 255 called the exit status or exit code. 0 means "it worked"; anything else means some kind of failure, and which number depends on the program. The shell stores the status of the last command in $?. You have been relying on this all along: if, while, and the [ command all speak in exit codes.
~% ls /etc/passwd > /dev/null; echo $?
0
~% ls /nope 2> /dev/null; echo $?
1
~% grep -q zzz /etc/passwd; echo $?
1
~% nosuchcommand 2> /dev/null; echo $?
127
~% /etc/passwd 2> /dev/null; echo $?
126
~% echo $?
0
Notice the last line: $? is replaced after every command, including the echo that printed it, so the second echo $? shows the status of the first one. If you need a status later, save it at once: status=$?.
| Code | Conventional meaning |
|---|---|
| 0 | Success |
| 1 | General failure (file missing, no match…) |
| 2 | Wrong usage / bad arguments (e.g. grep with a bad pattern) |
| 126 | Found but not executable |
| 127 | Command not found |
| 128 + N | Killed by signal N (130 = Ctrl+C) |
Setting your own status with exit
exit N ends the script immediately with status N. Without a number, exit reuses the status of the last command, and a script that simply runs off the end does the same. Pick small, documented codes: 0 for success, 1 for "it failed", 2 for "you called me wrong". Other scripts, cron, and && chains will treat your script correctly only if it reports honestly, so never end a failing script with exit 0.
~% cat lab/l89/exists.sh
#!/bin/sh
[ $# -eq 1 ] || { echo "usage: $0 FILE" >&2; exit 2; }
[ -e "$1" ] || exit 1
exit 0
~% lab/l89/exists.sh /etc/passwd; echo $?
0
~% lab/l89/exists.sh /nope; echo $?
1
~% lab/l89/exists.sh; echo $?
usage: lab/l89/exists.sh FILE
2
&& and ||
a && b runs b only if a succeeded; a || b runs b only if a failed. Read them as "and then" and "or else". They let you write whole decisions on one line: mkdir -p out && cp *.txt out/ copies only if the directory could be created; cp cfg backup || echo "backup failed" >&2 reports a problem. They evaluate left to right with equal priority, so a && b || c means "if a and b succeed, fine; if either fails, run c", which is *not* quite an if/else: when a works but b fails, c runs too.
Two shell options make scripts stricter. set -e aborts the script as soon as any command fails, so you do not continue deleting things after a cd that did not happen. set -x prints each command before running it, with its variables expanded, which is the fastest way to see what a script is really doing. Put set -e right after the shebang in scripts that change things; add set -x temporarily while debugging.
Commands in this lesson
| Command | What it does |
|---|---|
echo $? | Status of the last command. |
exit 2 | End the script with status 2. |
a && b | Run b only if a succeeded. |
a || b | Run b only if a failed. |
set -e | Abort the script on the first failure. |
set -x | Trace every command as it runs. |
true / false | Commands that always succeed / fail. |
Quiz
Which exit status means success?
- 1
- 0
- Any positive number
`cd /nope && rm -rf *` — what happens when /nope does not exist?
- `rm` runs in the current directory
- `rm` never runs, because `cd` failed
- The shell exits
What does status 127 mean?
- Command not found
- Permission denied
- Killed by Ctrl+C
What does `set -e` do in a script?
- Exports all variables
- Stops the script as soon as a command fails
- Echoes each command
`ls /nope; echo $?; echo $?` prints `1` and then…
- `1`
- `0`
- Nothing
Practice
Write an executable script `/root/lab/l89/three.sh` that prints nothing and exits with status 3.
Write an executable script `/root/lab/l89/exists.sh FILE` that prints nothing and exits 0 if FILE exists, or 2 if it does not.
On one line, try to copy `/root/lab/l89/missing.cfg` (which does not exist) to `/root/lab/l89/copy.cfg`, and if the copy fails write `copy failed` into `/root/lab/l89/error.log` using `||`.
Open this lesson in the app to do the tasks in a real Linux machine and have them checked.