Zsh Shell Tutorial 10: Loops — for, while, until & File Processing
Video: Zsh Shell Tutorial 10: Loops — for, while, until & File Processing by Taught by Celeste AI - AI Coding Coach
Zsh Lesson 10: Loops — for, while, until, File Processing
for x in list; do ... done.for ((i=0; i<10; i++))C-style.while [[ cond ]]; do ... done.untilis "while NOT."breakexits,continueskips to next iteration. The "read a file line by line" pattern:while IFS= read -r line; do ... done < file.
Loops are how scripts process collections — files, names, log entries, anything iterable.
for: list iteration
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
Output:
Fruit: apple
Fruit: banana
Fruit: cherry
for VAR in LIST; do ... done. Each iteration assigns VAR to the next item.
Brace expansion
for i in {1..5}; do
echo "Count: $i"
done
# Count: 1 ... Count: 5
for i in {0..20..5}; do # step 5
echo $i
done
# 0 5 10 15 20
{a..b} and {a..b..step} generate ranges before the loop runs.
C-style for
for ((i=0; i<3; i++)); do
echo "Index: $i"
done
Familiar from C/Java/JS. init; condition; update. Useful when you need an index, not values.
Iterating over files
for f in *.txt; do
echo "Processing: $f"
wc -l "$f"
done
Globs expand to filenames before the loop runs. Each iteration $f is one file.
for f in **/*.log; do
echo "$f"
done
**/ recursively matches subdirectories (Zsh extension; need setopt EXTENDED_GLOB in some versions).
Iterating over command output
for line in $(grep ERROR log.txt); do
echo "$line"
done
But this splits on whitespace, not lines. For real line-by-line processing, use while read (below).
while: until condition becomes false
count=1
while [[ $count -le 5 ]]; do
echo "Count: $count"
((count++))
done
Standard counting loop. Same as a for would do, just more explicit.
until: opposite of while
num=5
until [[ $num -eq 0 ]]; do
echo "Countdown: $num"
((num--))
done
echo "Liftoff!"
until cond runs the loop while the condition is FALSE. Stops when it becomes true. Less common than while, but occasionally clearer.
Reading a file line by line
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
This is the canonical "process a file" loop:
IFS=— preserve leading/trailing whitespace.-r— don't interpret backslashes.< input.txt— redirect file as stdin.
Process each line:
while IFS= read -r line; do
if [[ "$line" == *ERROR* ]]; then
echo "FAIL: $line"
fi
done < server.log
Reading from a command
ls *.txt | while IFS= read -r f; do
echo "Found: $f"
done
But: variables set inside this while don't persist after, because pipe creates a subshell. Workaround:
# Process substitution (Zsh/Bash extension)
while IFS= read -r f; do
count=$((count + 1))
done < <(ls *.txt)
echo "Total: $count"
Or just use a for with a glob.
break: exit early
for name in Alice Bob Charlie Dave; do
if [[ "$name" == "Charlie" ]]; then
echo "Found!"
break
fi
echo "Checking: $name"
done
break exits the innermost loop. break 2 exits two levels (nested loops).
continue: skip to next iteration
for i in {1..10}; do
if (( i % 2 == 0 )); then
continue # skip evens
fi
echo "Odd: $i"
done
continue jumps to the next iteration without running the rest of the body.
Infinite loops
while true; do
echo "tick"
sleep 1
done
while true runs forever. Use break (or Ctrl-C) to exit.
For services or watchers, this is the foundation.
A file processor
#!/usr/bin/env zsh
set -euo pipefail
if [[ $# -eq 0 ]]; then
echo "Usage: $0 <file>..." >&2
exit 1
fi
total_lines=0
for file in "$@"; do
if [[ ! -f "$file" ]]; then
echo "Skipping (not a file): $file" >&2
continue
fi
lines=$(wc -l < "$file")
total_lines=$((total_lines + lines))
echo "$file: $lines lines"
done
echo "Total: $total_lines lines"
Iterate over args; skip non-files; accumulate. Standard pattern.
Loop control with multiple conditions
attempt=0
max_attempts=5
while (( attempt < max_attempts )); do
if curl -sf "$URL" > result.json; then
break
fi
((attempt++))
sleep 5
done
if (( attempt == max_attempts )); then
echo "Failed after $max_attempts attempts" >&2
exit 1
fi
Retry pattern: try, on success break, on failure increment and sleep.
Iterating over array
fruits=(apple banana cherry)
for fruit in $fruits; do # Zsh: auto-expands
echo "$fruit"
done
for fruit in "${fruits[@]}"; do # portable form
echo "$fruit"
done
for ((i=1; i<=${#fruits}; i++)); do # by index (Zsh: 1-based)
echo "$i: $fruits[$i]"
done
Lesson 12 covers arrays in depth.
select: interactive menu
select choice in apple banana cherry quit; do
case "$choice" in
apple) echo "An apple a day..." ;;
banana) echo "Going bananas." ;;
cherry) echo "Cherry on top." ;;
quit) break ;;
*) echo "Invalid" ;;
esac
done
select builds a numbered menu and loops until you break. Quick way to add interactive menus to scripts.
Common stumbles
for line in $(cat file) splits on whitespace. Use while IFS= read -r line; do ... done < file.
Modifying state in piped while. cat file | while ...; var=...; done — var is in a subshell. Use process substitution < <(cmd) or store the count via tempfile.
((count++)) and set -e. (( count++ )) returns non-zero when count is 0 (because the expression evaluates to 0). With set -e, that exits the script. Use count=$((count + 1)) or (( count++, 1 )).
Forgot done. "syntax error: unexpected end of file." Every loop needs done.
Empty glob. for f in *.txt with no .txt files iterates once with f="*.txt" (literal). Set setopt nullglob for empty-list behavior, or check inside:
for f in *.txt; do
[[ -e "$f" ]] || continue
...
done
break in piped loops. cat | while; do break; done may not actually exit the outer pipeline cleanly. Use process substitution.
Slow loops in shell. Shell loops are not fast. For 10,000+ iterations, look at awk, xargs -P, or write the inner logic in a faster language.
What's next
Lesson 11: functions. Arguments, return values, scope.
Recap
for x in list; do ... done — list iteration. for ((i=0; i<n; i++)) — C-style index. while [[ cond ]]; do ... done, until for the inverse. while IFS= read -r line; do ... done < file for line-by-line file reading. break exits, continue skips. select for interactive menus. Watch for subshell pitfalls in piped loops; prefer process substitution < <(cmd).
Next lesson: functions.