Shell Scripting Fundamentals¶
Quoting: the single biggest source of shell script bugs¶
name="John Smith"
echo $name # prints: John Smith (looks fine here)
echo Hi $name # prints: Hi John Smith (still looks fine)
for word in $name; do echo "[$word]"; done
# prints: [John]
# [Smith] <- word-split into TWO iterations, not one
Unquoted variable expansion undergoes word splitting (breaking on
whitespace) and globbing (expanding * as a filename pattern) —
$name isn't substituted as one atomic string, it's substituted and
then re-parsed as if it were typed directly. This is invisible for
simple cases (a single command argument that happens not to contain
spaces) and breaks the moment a variable actually contains a space, a
*, or is empty.
for word in "$name"; do echo "[$word]"; done
# prints: [John Smith] <- one iteration, correct
Double-quoting a variable expansion ("$name") prevents both word
splitting and globbing while still expanding the variable's value —
this is why "$var" should be the default habit for essentially every
variable reference in a script, not an occasional defensive measure.
Single quotes ('$name') suppress expansion entirely — the variable
name is printed literally, not its value — useful specifically when a
literal $ needs to reach the output unexpanded.
Exit codes and set -euo pipefail¶
Every command returns an exit code: 0 means success, non-zero means
failure (the specific non-zero value's meaning is command-specific).
By default, a bash script keeps running after a command fails —
silently, unless something explicitly checks $? (the previous
command's exit code).
#!/usr/bin/env bash
set -euo pipefail
set -e— exit the script immediately if any command fails (returns non-zero), instead of continuing past it silently.set -u— treat referencing an undefined variable as an error, instead of silently substituting an empty string (which can turn a typo'd variable name into a script that runs with unintended empty arguments rather than failing loudly).set -o pipefail— without this, a pipeline (cmd1 | cmd2)'s exit code is onlycmd2's —cmd1failing is invisible ifcmd2still exits 0.pipefailmakes the whole pipeline's exit code reflect the first failure anywhere in it.
This three-flag combination is close to a mandatory default for any
script beyond a few trivial lines — without it, a script silently
continuing past a failed step (a failed cd, a failed download) and
then doing something destructive with wrong or missing data is a
common, hard-to-diagnose failure mode.
Conditionals and loops¶
if [ -f "$file" ]; then
echo "exists"
elif [ -d "$file" ]; then
echo "is a directory"
else
echo "does not exist"
fi
for f in *.log; do
echo "processing $f"
done
while read -r line; do
echo "line: $line"
done < input.txt
[ -f "$file" ] (or the more modern [[ -f "$file" ]], which handles
some edge cases like empty variables more gracefully and doesn't need
manual quoting protection to the same degree) tests file conditions —
-f (regular file exists), -d (directory exists), -z (string is
empty), -n (string is non-empty) are the common ones.
while read -r line is the standard way to process a file
line-by-line; the -r flag prevents backslashes in the input from
being interpreted as escape sequences, which is almost always the
intent when reading arbitrary text data.
Functions¶
deploy() {
local env="$1"
local version="$2"
echo "deploying $version to $env"
}
deploy "staging" "v1.2.3"
local scopes a variable to the function — without it, every variable
in a bash function is global by default, which is a common source of
one function's internal variable accidentally clobbering another
function's variable of the same name elsewhere in a larger script.
Common pitfall¶
Forgetting to quote a variable that holds a filename with a space (or
that could be empty) is the single most common way a shell script
works perfectly in every test the author tries and then breaks on a
real input — rm -rf $dir/* with an unset, empty $dir expands to
rm -rf /* (deleting from the filesystem root) rather than failing
loudly, precisely because an empty unquoted variable simply
disappears from the command line instead of being passed as an empty
argument. set -u (treating unset variables as errors) and
consistently quoting ("$dir") together close this specific,
genuinely dangerous class of bug.