Loops¶
Loops are what make the computer do in seconds what would take you hours manually — process 10,000 files with the same script that handles 1.
Learning Objectives¶
- Write
forloops over lists, ranges, and command output - Use
whileloops for condition-based repetition - Use
untilloops - Control loops with
breakandcontinue - Apply common real-world loop patterns for file processing
for Loops¶
Over a List¶
Over Files¶
Always quote $file
If a filename contains spaces, an unquoted $file will be word-split. Always use "$file" inside loops.
Over a Range¶
C-style for Loop¶
Over Command Output¶
Command substitution in for loops
for item in $(command) splits output on whitespace — filenames with spaces will break. For file processing, use while IFS= read -r line instead (see below).
while Loops¶
Reading a File Line by Line¶
This is the correct, safe way to process a file line by line. IFS= prevents leading/trailing whitespace from being stripped. -r prevents backslash interpretation.
Infinite Loop with Break¶
while true; do
read -rp "Enter 'quit' to exit: " input
if [[ "$input" == "quit" ]]; then
break
fi
echo "You typed: $input"
done
until Loops¶
until runs while the condition is false — the opposite of while.
A common use: wait for a service to become available:
until curl -sf http://localhost:8080/health; do
echo "Waiting for server..."
sleep 2
done
echo "Server is up."
break and continue¶
for i in {1..10}; do
if (( i == 5 )); then
continue # skip 5
fi
if (( i == 8 )); then
break # stop at 8
fi
echo "$i"
done
Real-World Loop Patterns¶
Batch File Rename¶
#!/usr/bin/env bash
set -euo pipefail
for file in *.jpeg; do
[[ -e "$file" ]] || continue # skip if no match
newname="${file%.jpeg}.jpg"
mv -- "$file" "$newname"
echo "Renamed: $file -> $newname"
done
Process CSV Records¶
#!/usr/bin/env bash
set -euo pipefail
while IFS=, read -r name email role; do
echo "User: $name <$email> — Role: $role"
done < users.csv
Retry Logic¶
#!/usr/bin/env bash
set -euo pipefail
MAX_ATTEMPTS=3
attempt=1
while (( attempt <= MAX_ATTEMPTS )); do
if some_flaky_command; then
echo "Success on attempt $attempt"
break
fi
echo "Attempt $attempt failed. Retrying..."
(( attempt++ ))
sleep 2
done
if (( attempt > MAX_ATTEMPTS )); then
echo "All $MAX_ATTEMPTS attempts failed" >&2
exit 1
fi
Common Mistakes¶
Forgetting to increment in while
A while loop without an increment is an infinite loop. Always make sure the condition can eventually become false.
for file in $(ls) — never do this
for file in $(ls) breaks on filenames with spaces. Use glob patterns instead: for file in *. Globs preserve filenames with spaces (when quoted).
[[ -e "$file" ]] || continue
When a glob like *.sh matches nothing, bash leaves it as the literal string *.sh. The [[ -e "$file" ]] || continue guard prevents the loop body from running on a non-existent file. Always include this guard when looping over globs.
Practice Exercises¶
Warm-Up (run and observe)¶
- Write a
forloop that prints the numbers 1 to 20 using a{1..20}range. - Use a
while IFS= read -r lineloop to print each line of/etc/hostnamewith a line number prefix. - Write a loop that prints every
.shfile in~/scripts/. What happens if there are no.shfiles?
Main (write a short script)¶
Create ~/scripts/bulk_rename.sh that adds a timestamp prefix to every .log file in a given directory:
#!/usr/bin/env bash
set -euo pipefail
DIR="${1:?Usage: $0 <directory>}"
TIMESTAMP=$(date +%Y%m%d)
count=0
for file in "$DIR"/*.log; do
[[ -e "$file" ]] || { echo "No .log files found in $DIR"; exit 0; }
base="${file##*/}"
newname="$DIR/${TIMESTAMP}_${base}"
mv -- "$file" "$newname"
echo "Renamed: $base -> ${TIMESTAMP}_${base}"
(( count++ ))
done
echo "Renamed $count file(s)."
Stretch¶
- Write a script that reads a list of URLs from a file (one per line) and downloads each one using
curl, skipping any that fail. - Explain what
while IFS= read -r line; do ... done < filedoes at each step. Why isIFS=set to empty? Why-r? - Research
mapfile(also calledreadarray). When is it more appropriate than awhile readloop?
Interview Questions¶
- Why should you avoid
for file in $(ls)?
Show answer
$(ls) is word-split on whitespace, so filenames containing spaces are treated as multiple items. Glob patterns like for file in * handle spaces correctly when the loop variable is quoted ("$file"). Also, ls output is meant for human reading, not programmatic processing.
- What is the safe pattern for reading a file line by line?
Show answer
while IFS= read -r line; do ...; done < file. IFS= prevents leading and trailing whitespace from being stripped from each line. -r prevents backslash characters from being interpreted as escape sequences. Redirecting < file at the done avoids subshell issues that would prevent variables set inside the loop from being visible outside it.
- What is the difference between
breakandcontinue?
Show answer
break exits the innermost loop entirely. continue skips the rest of the current iteration and moves to the next one. Both accept an optional numeric argument (break 2) to break out of nested loops — break 2 exits the two innermost loops.