Why this lesson
Every decision a script makes - "did the copy work?", "is nginx running?" - comes down to an exit status. Get the mechanics wrong and the script checks the wrong command, or loses a variable it just set. This lesson is the plumbing under if, pipes and $(...), plus case for choosing between several values.
What you need to know already: exit status and $?, && / || (1.7); if, $(cmd) and PIPESTATUS (6.3); systemctl is-active (2.1).
if runs a command
if, while and &&/|| do not test "expressions" - they run a command and branch on its exit status (0 = true). The [[ ]] test you will meet in 6.10 is just another command that happens to compute a status:
if grep -q "^learner:" /etc/passwd; then echo "exists"; fi # grep IS the test
if systemctl is-active --quiet nginx; then ...
if ! mkdir "$dir"; then echo "cannot create $dir" >&2; exit 1; fi
(^learner: means "a line that starts with learner:" - ^ is "start of line".)
So this common pattern is a detour (shellcheck SC2181):
some_command
if [ $? -ne 0 ]; then ... # checks $?... unless someone adds a line in between
if ! some_command; then ... # say what you mean
Many tools have a quiet mode made for this - no output, only the status: grep -q, systemctl is-active --quiet, diff -q (do two files differ?), cmp -s (are two files byte-identical?).
Every stage of a pipeline has a status
$ false | true | false; echo "${PIPESTATUS[@]}"
1 0 1
$ ls /nope | wc -l; echo "last=$? stages=${PIPESTATUS[*]}"
ls: cannot access '/nope': No such file or directory
0
last=0 stages=2 0
The first line: three stages, statuses 1, 0 and 1. The second: ls failed with 2, wc -l (count lines) printed 0 and succeeded, so $? says 0.
$? is the last stage (or, with pipefail, the last failing one). PIPESTATUS is an array with every stage (${PIPESTATUS[@]} = all of them; arrays are in 6.12) - and, like $?, it is overwritten by the next command, so copy it immediately: st=("${PIPESTATUS[@]}").
( ) is a new shell; { } is not
$ ( cd /; pwd ); pwd
/
/home/learner
$ { cd /tmp; pwd; }; pwd
/tmp
/tmp
Parentheses run the commands in a subshell: a copy of the shell, started as a child process (3.1), whose cd, variables and exit stay inside it and vanish when it ends. Braces group commands in the current shell (note the spaces and the final ;). Use ( ) deliberately to contain a cd or a set -x (tracing, 6.22); use { } to redirect a group's output at once.
Subshells are also created, less visibly, by:
$(...)- command substitution.x=$(cd /tmp; pwd)does not move you.- every side of a pipe - so a loop on the right of
|loses its variables (6.8 shows the fix). &- background jobs (3.10).
An exit inside $(...) exits only the substitution, but its status becomes the status of the assignment:
$ x=$( exit 3 ); echo $?
3
That is exactly why x=$(cmd) works with set -e - and local x=$(cmd) does not (6.3): there, local's own status replaces it.
Command substitution also strips all trailing newlines (printf is below; \n in it means newline):
$ echo "$(printf 'a\n\n\n')|"
a|
case: pattern matching on a value
case compares one value against a list of patterns and runs the branch of the first one that matches - a tidy replacement for a chain of ifs:
case "$env" in
dev|test) replicas=1 ;;
staging) replicas=2 ;;
prod-*) replicas=6 ;;
"") echo "env is empty" >&2; exit 2 ;;
*) echo "unknown env: $env" >&2; exit 2 ;;
esac
- Patterns are globs (filename-style patterns):
*any text,?any one character,[abc]one of those characters, and|between alternatives.caseends withesac(case backwards). - The first match wins;
*)last is the default. - Each branch ends with
;;. (;&falls through,;;&tests the next pattern - rarely needed.) - Quote the word being tested (
"$env"); do not quote a pattern you want to match as a glob.
case is the right tool for dispatching on a subcommand (start|stop|status) and for option parsing; long if/elif chains of string comparisons usually want to be a case.
printf, not echo, for data
$ var='-n'
$ echo "$var" # prints nothing: echo took it as an option
$ printf '%s\n' "$var"
-n
$ printf '%-10s %5d\n' orders 42 payments 7
orders 42
payments 7
printf FORMAT ARGS... prints its arguments according to a format string: %s is "a string goes here", %d "an integer goes here", \n a newline. %-10s pads to 10 characters, left-aligned; %5d pads a number to 5, right-aligned.
echo behaves differently across shells and treats data that looks like an option as an option. printf is the same everywhere, reuses the format for extra arguments (the second example printed two lines), and does column formatting. Use echo for fixed messages, printf for anything containing a variable you did not write yourself.
Exit codes you should produce
0 success
1 generic failure
2 usage error (bad arguments) - see 6.20
3-125 your own, documented
and the ones the shell produces for you: 126 not executable, 127 not found, 128+N killed by signal N. Keep your own below 126 so they can never be confused with those.
What you can now do
- Branch on a command directly (
if ! cmd) instead of checking$?later. - Say which constructs run in a subshell, and why variables set there vanish.
- Dispatch on a value with
case, and print data safely withprintf.