Loops In Depth
Master for over arrays and globs, C-style for loops, while read for line-by-line processing, break/continue, and until.
- Iterate over arrays, globs, and command output with for
- Use C-style for ((i=0; i<n; i++)) for counted loops
- Process a file line by line with while read
- Control loop flow with break and continue
- Use until as a "while not" construct
Conditionals tell a script what to do; loops tell it how many times. Bash has a richer set of loop forms than most people expect — each one fits a different problem. Knowing which form to reach for is what turns a stack of repeated commands into a clean, maintainable script.
for over lists, arrays, and globs
The basic for loop iterates over a whitespace-separated list:
for color in red green blue; do
echo "$color"
doneMore practically, iterate over an array:
files=("report.csv" "data.json" "config.yaml")
for f in "${files[@]}"; do
echo "Processing $f"
doneUse "${files[@]}" (with the double quotes and @) to preserve filenames that contain spaces. ${files[*]} without quotes will split on spaces and break.
Globs expand inline, making it easy to operate on a set of files:
for script in /usr/local/bin/*.sh; do
[[ -x "$script" ]] && echo "$script is executable"
doneIf no files match the glob, Bash passes the literal pattern string into the loop body. Guard against this with [[ -e "$script" ]] or set nullglob at the top of the script.
Iterating over command output
Command substitution feeds into a for loop:
for user in $(cut -d: -f1 /etc/passwd); do
echo "User: $user"
doneWord splitting bites here. If any output line contains spaces, the loop treats each word as a separate iteration. For line-by-line handling of command output — especially when values can contain spaces — use while read instead (shown below).
C-style for loops
When you need a counted loop with an index, the C-style form is the clearest choice:
for ((i = 0; i < 5; i++)); do
echo "Step $i"
done
# Counting down
for ((i = 10; i > 0; i -= 2)); do
echo "$i..."
done
echo "Done"You can also declare multiple variables, use modulo, or reference array indices:
arr=(a b c d e)
for ((i = 0; i < ${#arr[@]}; i++)); do
echo "arr[$i] = ${arr[$i]}"
donewhile read for line-by-line processing
while read is the right tool whenever you need to process a file (or command output) one line at a time — especially when lines can contain spaces:
while IFS= read -r line; do
echo "Line: $line"
done < /etc/hostsTwo details matter:
IFS=prevents leading/trailing whitespace from being stripped.-rprevents backslashes from being treated as escape sequences.
Feed command output the same way:
find /var/log -name "*.log" | while IFS= read -r logfile; do
wc -l "$logfile"
donewhile read in a pipeline runs in a subshell. Any variables you set inside the loop are not visible outside it. If you need the variable after the loop, use process substitution (while read ... done < <(command)) or redirect from a file.
break and continue
break exits the current loop immediately. continue skips the rest of the current iteration and moves to the next:
for f in *.log; do
[[ ! -s "$f" ]] && continue # skip empty files
[[ "$f" = error.log ]] && break # stop at error.log
echo "Reading $f"
doneBoth accept a numeric argument to break out of nested loops: break 2 exits two levels.
until
until is the mirror of while — it loops as long as the condition is false, stopping when it becomes true. It reads naturally for "wait until something is ready":
until [[ -f /tmp/service.ready ]]; do
echo "Waiting for service..."
sleep 2
done
echo "Service is up"A while equivalent (while [[ ! -f ... ]]) works identically — until is just more expressive when the "done" condition is what you care about.
Check your understanding
- 1.You have arr=("file one.txt" "file two.txt"). Which expansion correctly iterates over both elements without splitting on spaces?
- 2.What does the -r flag do in while IFS= read -r line?
- 3.A variable set inside a while read loop that receives input via a pipe is visible after the loop ends.
Do it yourself
# Iterate over files with spaces in names
mkdir -p /tmp/looptest
touch "/tmp/looptest/file one.txt" "/tmp/looptest/file two.txt"
files=("/tmp/looptest/file one.txt" "/tmp/looptest/file two.txt")
for f in "${files[@]}"; do echo "Found: $f"; done
# C-style countdown
for ((i = 5; i > 0; i--)); do echo "$i"; done
# Process /etc/hosts line by line
while IFS= read -r line; do
[[ "$line" =~ ^# ]] && continue # skip comments
echo "$line"
done < /etc/hosts
# until a file appears
touch /tmp/ready_signal
until [[ -f /tmp/ready_signal ]]; do sleep 1; done && echo "ready"
rm /tmp/ready_signalWhere to go next
With precise conditionals and every loop form in your toolkit, the next lesson — Functions — shows how to package reusable logic, manage local state, and compose scripts that don't repeat themselves.