Skip to content

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 for loops over lists, ranges, and command output
  • Use while loops for condition-based repetition
  • Use until loops
  • Control loops with break and continue
  • Apply common real-world loop patterns for file processing

for Loops

Over a List

for fruit in apple banana cherry; do
    echo "Fruit: $fruit"
done
Fruit: apple
Fruit: banana
Fruit: cherry

Over Files

for file in ~/scripts/*.sh; do
    echo "Found script: $file"
done

Always quote $file

If a filename contains spaces, an unquoted $file will be word-split. Always use "$file" inside loops.

Over a Range

for i in {1..5}; do
    echo "Count: $i"
done
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5

for i in {0..20..5}; do    # start..end..step
    echo "$i"
done
0
5
10
15
20

C-style for Loop

for (( i=0; i<5; i++ )); do
    echo "Index: $i"
done

Over Command Output

for user in $(cut -d: -f1 /etc/passwd); do
    echo "User: $user"
done

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

count=1
while [[ $count -le 5 ]]; do
    echo "Count: $count"
    (( count++ ))
done

Reading a File Line by Line

while IFS= read -r line; do
    echo "Line: $line"
done < /etc/passwd

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.

# Process command output line by line
df -h | while IFS= read -r line; do
    echo ">>> $line"
done

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.

count=1
until [[ $count -gt 5 ]]; do
    echo "Count: $count"
    (( count++ ))
done

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
1
2
3
4
6
7


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)

  1. Write a for loop that prints the numbers 1 to 20 using a {1..20} range.
  2. Use a while IFS= read -r line loop to print each line of /etc/hostname with a line number prefix.
  3. Write a loop that prints every .sh file in ~/scripts/. What happens if there are no .sh files?

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

  1. 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.
  2. Explain what while IFS= read -r line; do ... done < file does at each step. Why is IFS= set to empty? Why -r?
  3. Research mapfile (also called readarray). When is it more appropriate than a while read loop?

Interview Questions

  1. 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.

  1. 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.

  1. What is the difference between break and continue?
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.


day04-part1-control-flow | day05-part1-io-pipes