Read

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

CodeConventional meaning
0Success
1General failure (file missing, no match…)
2Wrong usage / bad arguments (e.g. grep with a bad pattern)
126Found but not executable
127Command not found
128 + NKilled 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

CommandWhat it does
echo $?Status of the last command.
exit 2End the script with status 2.
a && bRun b only if a succeeded.
a || bRun b only if a failed.
set -eAbort the script on the first failure.
set -xTrace every command as it runs.
true / falseCommands that always succeed / fail.

Quiz

  1. Which exit status means success?

    • 1
    • 0
    • Any positive number
  2. `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
  3. What does status 127 mean?

    • Command not found
    • Permission denied
    • Killed by Ctrl+C
  4. What does `set -e` do in a script?

    • Exports all variables
    • Stops the script as soon as a command fails
    • Echoes each command
  5. `ls /nope; echo $?; echo $?` prints `1` and then…

    • `1`
    • `0`
    • Nothing

Practice

  1. Write an executable script `/root/lab/l89/three.sh` that prints nothing and exits with status 3.

  2. 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.

  3. 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.