for loops
for iterates over words or uses arithmetic syntax. {1..5} works for a literal range; when the limit is a variable, use for ((i=1; i<=n; i++)) because brace expansion happens before parameter expansion. seq 1 "$n" is another option that prints a sequence.
for i in {1..3}; do printf '%s\n' "$i"; done
for f in /var/log/*.log; do echo "checking $f"; done
for host in web1 web2 db1; do ssh "$host" uptime; done
A glob in for gives one file name per iteration, even names with spaces, so it is safer than looping over $(ls). If nothing matches, the pattern itself is returned; check [[ -e $f ]] inside the loop.
flowchart LR
L["for f in *.log<br/>each item"] --> B["Loop body<br/>gzip $f"]
B --> N{"More items?"}
N -- "yes" --> L
N -- "no" --> D["done"]
W["while read -r line<br/>until input ends"] --> B
CA["case $1 in<br/>start or stop"] --> PAT["first matching<br/>pattern runs"]
%% step L: for walks a fixed list, such as filenames from a glob.
%% step B: The body runs once per item; break and continue control the loop.
%% step N: The loop repeats until the list is used up.
%% step W: while read -r processes input line by line, for files or command output.
%% step CA: case compares a value against shell patterns.
%% step PAT: Only the first matching branch runs; *) catches everything else.
while and until
while IFS= read -r line; do printf '%s\n' "$line"; done < file.txt
while IFS=, read -r name port; do echo "$name -> $port"; done < hosts.csv
IFS= preserves leading and trailing whitespace, while read -r preserves backslashes. Setting IFS=, splits each CSV line into fields. Redirection after done supplies input to the whole loop and avoids a pipeline subshell in many cases.
while repeats while a command succeeds; until repeats while it fails, which suits waiting:
until curl -sf http://localhost:8080/health; do sleep 2; done
Controlling a loop
break leaves the loop and continue skips to the next iteration. while true; do ...; sleep 60; done creates a simple polling loop; stop it with Ctrl+C. Always make sure a while loop can end, for example with a retry counter.
case
case matches shell patterns; each arm normally ends with ;;, and *) is the fallback:
case $action in
start|up) echo starting ;;
stop) echo stopping ;;
*.conf) echo "config file" ;;
*) echo unknown ;;
esac
case reads more clearly than a long if/elif chain when one value is compared against many options, and it is the usual way to handle a script's subcommands such as start, stop, and status.
Patterns use glob semantics, not regular expressions by default. Quote the tested value when appropriate, but do not quote a wildcard pattern if you want pattern matching.