Conditionals & loops
Key takeaways
Control flow is what turns a list of commands into a program that can think.
if / test [ ] lets a script make a decision — run this block only when a
condition holds. for repeats a block once per item in a list or glob, and
while repeats as long as a condition stays true. Together they let a
script decide and repeat instead of
running the same lines blindly from top to bottom.
A plain script runs every line in order, no matter what. That is fine until you need it to react — back up a file only if it exists, process every capture in a folder, keep retrying until something comes up. Those three needs map onto the three tools in this lesson: decisions, and two kinds of repetition.
Making decisions — if
An if statement runs a block only when a condition is met. The shape is
always the same, and it closes with fi (if backwards):
if [ -f config.toml ]; then
echo "found the config"
elif [ -f config.yaml ]; then
echo "found a YAML config instead"
else
echo "no config here"
fi
Read it top to bottom: the first condition that holds wins, its then block
runs, and the rest are skipped. elif (“else if”) lets you test more
conditions, and else is the catch-all when none matched. Only if and fi
are required — elif and else are optional.
The test command
The thing between the brackets is the test command. [ ... ] is actually a
program (its condition goes inside), which is why it needs spaces around the
brackets. In bash you can also write [[ ... ]], a safer builtin that copes with
unquoted variables and adds pattern matching — prefer it in bash scripts, and
keep [ ] for scripts that must run under plain sh.
Test comes in three flavours — files, strings, and numbers:
| Test | True when |
|---|---|
[ -f path ] |
path exists and is a regular file |
[ -d path ] |
path exists and is a directory |
[ -e path ] |
path exists (any type) |
[ -z "$s" ] |
string $s is empty (zero length) |
[ "$a" = "$b" ] |
strings are equal |
[ "$a" != "$b" ] |
strings are not equal |
[ "$n" -eq 5 ] |
number equals 5 |
[ "$n" -lt 5 ] |
number is less than 5 |
[ "$n" -gt 5 ] |
number is greater than 5 |
Note the split: strings compare with = and !=, but numbers use the word
operators -eq, -lt, -gt. Always quote your variables ("$s") so an empty
value does not break the brackets.
for loops
A for loop runs its block once for each item in a list. The classic use is
looping over a glob — the shell expands the pattern to matching filenames
first, then hands them to the loop one at a time:
for f in *.log; do
echo "checking $f"
done
The list can be anything, not just files — spell it out inline, or loop over the
script’s own arguments with "$@":
for name in alpha bravo charlie; do
echo "$name"
done
for arg in "$@"; do
echo "you passed: $arg"
done
Quote the loop variable ("$f") so a filename with spaces stays a single item.
while loops
A while loop repeats as long as its condition stays true — useful when you do
not know the count in advance:
n=1
while [ "$n" -le 3 ]; do
echo "attempt $n"
n=$((n + 1))
done
The most common real use is reading a file line by line, pairing while
with the read command:
while read -r line; do
echo "got: $line"
done < channels.txt
read pulls one line into line each pass and the loop ends at end of file.
The -r flag stops backslashes being mangled, and < channels.txt feeds the
file in as input.
A worked example
Here is control flow doing something real: back up every .conf file in the
current folder, but only the ones that actually exist as regular files.
#!/usr/bin/env bash
for f in *.conf; do
if [ -f "$f" ]; then
cp "$f" "$f.bak"
echo "backed up $f"
fi
done
The for loop walks each .conf name; the if [ -f "$f" ] guard means a
pattern that matched nothing (left as the literal *.conf) is skipped rather
than copied by mistake. Decision and repetition together — the whole point of
this lesson in five lines.
Conditions are exit codes
Here is the idea that ties it together: if does not test true or false, it
tests whether a command succeeded. Every command reports an exit code —
0 for success, non-zero for failure — and if runs its then block when the
code is 0. [ ... ] is just a command that exits 0 when its condition holds.
That means you can test any command, not only [ ]:
if grep -q ERROR log.txt; then
echo "the log has errors"
fi
grep -q prints nothing but exits 0 when it finds a match — perfect for an
if. Exit codes are the currency of shell control flow, and the next lesson on
functions, exit codes & error handling
digs into them properly.
Quick check: which builds a loop that runs once per .log file in the folder?
Recap
if ... then ... firuns a block only when a condition holds; addelifandelsefor more branches.- The test command
[ ... ](and bash’s safer[[ ... ]]) checks files (-f,-d,-e), strings (-z,=,!=), and numbers (-eq,-lt,-gt). forrepeats once per item in a list or glob; loop over arguments with"$@".whilerepeats while a condition holds — including reading a file withwhile read -r line.- Quote your variables (
"$f") so spaces and empty values do not break things. ifreally tests a command’s exit code —0means success.