Scripts become useful when they make decisions and repeat work. Bash provides if, case, for, while and until, but its test syntax has three different forms and several traps around quoting and word splitting. This lesson teaches the modern forms — [[ ]] for strings and files, (( )) for numbers — and the loop patterns that handle files with spaces and read input line by line correctly.
In bash, a condition is any command. if runs it and checks the exit status: 0 means true, anything else means false. This is why if grep -q pattern file works with no brackets at all.
if grep -q "ERROR" app.log; then
echo "errors found"
fi
if ! ping -c1 -W1 example.com > /dev/null 2>&1; then
echo "offline" >&2
fi[ ] (the test command), [[ ]] and (( )) are simply commands that return 0 or 1.
[[ ]] is bash's built-in test. Unlike the older [ ], it does not word-split variables, supports &&/|| inside, pattern matching with == and regex matching with =~.
if [[ "$answer" == "yes" ]]; then echo "confirmed"; fi
if [[ -z "$name" ]]; then echo "name is empty"; fi
if [[ "$file" == *.tar.gz ]]; then echo "gzipped tarball"; fi # glob pattern
if [[ "$email" =~ ^[^@]+@[^@]+\.[a-z]+$ ]]; then echo "looks valid"; fi| Test | True when |
|---|---|
| -e path | exists |
| -f path / -d path / -L path | regular file / directory / symlink |
| -s path | exists and is not empty |
| -z "$s" / -n "$s" | string is empty / not empty |
| "$a" == "$b", != | string equal / not equal |
Use (( )) for numeric comparison rather than the -eq, -lt family, which are still valid inside [[ ]] but harder to read.
if (( count > 100 )); then echo "busy"; fi
if (( a >= 1 && a <= 10 )); then echo "in range"; fiif [[ -f "$config" ]]; then
source "$config"
elif [[ -f "/etc/app/default.conf" ]]; then
source /etc/app/default.conf
else
echo "no configuration found" >&2
exit 1
fi
[[ -d "$dir" ]] || mkdir -p "$dir" # create if missing
[[ -f .env ]] && source .env # load if presentThe &&/|| short forms are ideal for one-line guards. Do not chain a && b || c as an if/else; if b fails, c runs too.
case compares a value against glob patterns and is cleaner than an elif ladder for command dispatch and option parsing.
case "$1" in
start) systemctl start app ;;
stop|halt) systemctl stop app ;;
restart) systemctl restart app ;;
status) systemctl status app ;;
-h|--help) echo "usage: $0 {start|stop|restart|status}" ;;
*) echo "unknown command: $1" >&2; exit 2 ;;
esac
case "$OSTYPE" in
linux*) pkg="apt" ;;
darwin*) pkg="brew" ;;
esacEach branch ends with ;;. Patterns support | for alternatives and the usual glob characters.
for iterates over a list of words: literal values, a glob, an array, or command output.
for env in dev staging prod; do
echo "deploying to $env"
done
for f in *.jpg; do
[[ -e "$f" ]] || continue # guard against no matches
convert "$f" -resize 50% "thumb_$f"
done
for i in {1..5}; do echo "attempt $i"; done
for (( i = 0; i < 10; i += 2 )); do echo "$i"; done # C-style
for host in "${servers[@]}"; do ssh "$host" uptime; doneNever loop over $(ls) or unquoted $(cat file); filenames with spaces will split. Use a glob for files and while read for lines.
while repeats as long as the condition succeeds; until repeats until it succeeds.
n=0
while (( n < 3 )); do
echo "tick $n"; (( n++ ))
done
until curl -sf http://localhost:3000/health > /dev/null; do
echo "waiting for the app..."; sleep 2
done
while true; do
date; sleep 60
doneThe canonical pattern uses read -r (do not interpret backslashes) with IFS= (keep leading and trailing whitespace) and redirects the file into the loop.
while IFS= read -r line; do
echo "line: $line"
done < servers.txt
while IFS=, read -r name email country; do
echo "$name <$email> ($country)"
done < <(tail -n +2 users.csv) # skip the header via process substitution
find . -name "*.log" -print0 | while IFS= read -r -d '' f; do
gzip "$f"
doneIf a command inside the loop also reads stdin (ssh, ffmpeg), it will consume your file. Give it < /dev/null or use ssh -n.
break leaves the loop; continue skips to the next iteration. Both accept a number to affect enclosing loops (break 2).
for f in *.csv; do
[[ "$f" == "template.csv" ]] && continue
grep -q "ERROR" "$f" && { echo "bad file: $f"; break; }
process "$f"
doneif [ $var == "x" ] with an empty $var, which becomes [ == "x" ] and errors. Quote it or use [[ ]].[[$a == $b]] is a syntax error; the brackets are commands and need spaces around them.= for numbers or -eq for strings.for line in $(cat file) splitting on every space instead of every line.while read and then wondering why variables set inside are empty afterwards — the pipe runs the loop in a subshell. Use < file or < <(cmd).Which test is true when a variable holds an empty string?
[[ ]] for strings, files and patterns, and (( )) for numbers; always quote variables inside tests.case dispatches on glob patterns and is cleaner than long elif chains.for loops over words, globs or arrays; guard globs with [[ -e "$f" ]] || continue.while IFS= read -r line; do ...; done < file, never with for line in $(cat file).Next lesson: Bash Functions, Arguments and Exit Codes — structure scripts into reusable functions and communicate success or failure to callers.