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
TestTrue when
-f fileIt exists and is a regular file
-d dirIt exists and is a directory
-r / -w / -xReadable, writable or executable
-z "$s" / -n "$s"The string is empty or not empty
"$a" == "$b"The strings are equal
$n -lt 10Numeric 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, case inside a while loop stays readable up to about a dozen options; beyond that use getopts.

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 -r stops backslashes being swallowed and IFS= preserves leading and trailing spaces.
  • A pipeline runs in a subshell, so variables set inside cmd | while read are 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.

Variables, quoting and exit codes Writing a robust script

Last refreshed 2026-09-18.