Conditionals and loops
Branch on files, strings and numbers, match patterns with case, and iterate over lines without breaking on spaces.
Conditionals
if [[ -f "$config" ]]; then
echo "using $config"
elif [[ -d "$config" ]]; then
echo "$config is a directory, expected a file" >&2
exit 1
else
echo "no config found, using defaults"
fi
[[ "$env" == "prod" ]] || { echo "refusing to run outside prod" >&2; exit 1; }
if (( count > 5 )); then
echo "many"
fi| Test | True when |
|---|---|
-f file | It exists and is a regular file |
-d dir | It exists and is a directory |
-r / -w / -x | Readable, writable or executable |
-z "$s" / -n "$s" | The string is empty or not empty |
"$a" == "$b" | The strings are equal |
$n -lt 10 | Numeric comparison: -lt -le -gt -ge -eq -ne |
Prefer [[ ]] over [ ]. It does not word-split unquoted variables and it supports &&, || and pattern matching natively.
Case statements
case "$1" in
start|up)
start_service
;;
stop)
stop_service
;;
-*)
echo "unknown flag: $1" >&2
exit 2
;;
*)
echo "usage: $0 {start|stop}" >&2
exit 2
;;
esac- Patterns use glob syntax:
*,?and[a-z], with alternatives separated by|. - Always include a
*)branch. An unmatched value silently doing nothing is a classic support ticket. ;;ends a branch, while;&falls through into the next one.- For flags with values,
caseinside awhileloop stays readable up to about a dozen options; beyond that usegetopts.
Loops
for host in web1 web2 web3; do
ssh "$host" uptime
done
for file in ./*.log; do
[[ -e "$file" ]] || continue # no matches: the glob stays literal
gzip "$file"
done
for ((i = 0; i < 3; i++)); do echo "$i"; done
while IFS= read -r line; do
echo "line: $line"
done < input.txt
while IFS= read -r -d '' path; do # safe for any filename
echo "$path"
done < <(find . -type f -print0)read -rstops backslashes being swallowed andIFS=preserves leading and trailing spaces.- A pipeline runs in a subshell, so variables set inside
cmd | while readare lost afterwards. Redirect into the loop or use process substitution. - Give any loop that waits on an external system a break condition or a timeout.
⚠️
Do not loop over
$(command) when the data can contain spaces: word splitting will silently break each record into pieces. Use while IFS= read -r line instead.FAQ
Why is a variable empty after my while loop?
The loop ran in a subshell because it sat on the right of a pipe. Read from a file instead:
while read -r line; do ...; done < file, or use process substitution.How do I loop over the output of a command?
Avoid
for x in $(...) if the data can contain spaces or glob characters. Use while IFS= read -r line so every line survives intact.Related
Variables, quoting and exit codes Writing a robust script
Last refreshed 2026-09-18.