Lesson 4: Loops & Iteration

Goal: Learn to repeat actions efficiently with for, while, and until loops.


Table of Contents

  1. for-in Loops
  2. C-Style for Loops
  3. while Loops
  4. until Loops
  5. Reading Files Line by Line
  6. Loop Control — break & continue
  7. Infinite Loops & Retry Patterns
  8. Practical Examples
  9. Exercises

for-in Loops

Loop over a list

for color in red green blue; do
    echo "Color: $color"
done

Loop over a range

# Brace expansion
for i in {1..5}; do
    echo "Number: $i"
done

# With step
for i in {0..20..5}; do
    echo "Value: $i"  # 0, 5, 10, 15, 20
done

Loop over files

# All .txt files in current directory
for file in *.txt; do
    echo "Processing: $file"
done

# All files in a directory
for file in /var/log/*.log; do
    echo "Log: $file ($(du -h "$file" | cut -f1))"
done

# Safely handle "no match" case
shopt -s nullglob  # empty loop if no matches
for file in *.xyz; do
    echo "Found: $file"
done

Loop over command output

# Loop over lines of command output
for user in $(cut -d: -f1 /etc/passwd); do
    echo "User: $user"
done

# Loop over arguments
for arg in "$@"; do
    echo "Argument: $arg"
done

C-Style for Loops

Classic counter loops using C syntax:

# Count from 0 to 9
for ((i=0; i<10; i++)); do
    echo "i = $i"
done

# Count down
for ((i=10; i>0; i--)); do
    echo "Countdown: $i"
done

# Step by 2
for ((i=0; i<=20; i+=2)); do
    echo "Even: $i"
done

while Loops

Run as long as a condition is true.

Basic while

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

While with command exit code

# Wait for a file to appear
while [[ ! -f /tmp/ready.flag ]]; do
    echo "Waiting for ready flag..."
    sleep 2
done
echo "Ready!"

While with arithmetic

N=1
while ((N <= 100)); do
    echo "$N"
    ((N *= 2))
done
# Output: 1, 2, 4, 8, 16, 32, 64

until Loops

Run until a condition becomes true (opposite of while).

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

Practical: Wait for a service

#!/bin/bash
# Wait until PostgreSQL is ready
until pg_isready -h localhost -p 5432 2>/dev/null; do
    echo "Waiting for database..."
    sleep 2
done
echo "Database is ready!"

Reading Files Line by Line

Standard pattern

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

Why IFS= read -r?

  • IFS= prevents trimming leading/trailing whitespace
  • -r prevents backslash interpretation

Read from a file

# Process /etc/passwd
while IFS=: read -r user _ uid gid _ home shell; do
    if ((uid >= 1000)); then
        echo "User: $user (UID: $uid, Shell: $shell)"
    fi
done < /etc/passwd

Read from command output (process substitution)

while IFS= read -r line; do
    echo "Process: $line"
done < <(ps aux | grep nginx)

Read CSV data

cat > data.csv << 'EOF'
Name,Age,City
Alice,30,Berlin
Bob,25,Munich
Carol,28,Hamburg
EOF

# Skip header, read CSV
tail -n +2 data.csv | while IFS=, read -r name age city; do
    echo "$name is $age years old and lives in $city"
done

rm data.csv

Loop Control — break & continue

break — Exit the loop

for i in {1..100}; do
    if [[ $i -eq 5 ]]; then
        echo "Found 5, stopping"
        break
    fi
    echo "Number: $i"
done
# Output: 1, 2, 3, 4, Found 5, stopping

continue — Skip to next iteration

for i in {1..10}; do
    if ((i % 3 == 0)); then
        continue  # skip multiples of 3
    fi
    echo "Number: $i"
done
# Output: 1, 2, 4, 5, 7, 8, 10

Practical: Skip dotfiles

for file in *; do
    # Skip hidden files
    [[ "$file" == .* ]] && continue

    echo "Processing: $file"
done

Infinite Loops & Retry Patterns

Infinite loop

while true; do
    echo "Running... (Ctrl+C to stop)"
    sleep 5
done

Retry with max attempts

#!/bin/bash
set -euo pipefail

MAX_RETRIES=5
RETRY_DELAY=3
URL="https://api.example.com/health"

for ((attempt=1; attempt<=MAX_RETRIES; attempt++)); do
    echo "Attempt $attempt/$MAX_RETRIES..."

    if curl -sf "$URL" > /dev/null 2>&1; then
        echo "Service is up!"
        break
    fi

    if ((attempt == MAX_RETRIES)); then
        echo "Failed after $MAX_RETRIES attempts" >&2
        exit 1
    fi

    echo "Retrying in ${RETRY_DELAY}s..."
    sleep "$RETRY_DELAY"
done

Docker wait-for pattern

#!/bin/bash
set -euo pipefail

HOST="${DB_HOST:-db}"
PORT="${DB_PORT:-5432}"
TIMEOUT="${WAIT_TIMEOUT:-30}"

echo "Waiting for $HOST:$PORT (timeout: ${TIMEOUT}s)..."

START=$(date +%s)
while ! nc -z "$HOST" "$PORT" 2>/dev/null; do
    ELAPSED=$(( $(date +%s) - START ))
    if ((ELAPSED >= TIMEOUT)); then
        echo "Timeout waiting for $HOST:$PORT" >&2
        exit 1
    fi
    sleep 1
done

echo "$HOST:$PORT is available (took ${ELAPSED}s)"

Practical Examples

Batch rename files

#!/bin/bash
# Rename all .jpeg to .jpg
for file in *.jpeg; do
    [[ -f "$file" ]] || continue
    mv "$file" "${file%.jpeg}.jpg"
    echo "Renamed: $file → ${file%.jpeg}.jpg"
done

Backup multiple databases

#!/bin/bash
set -euo pipefail

BACKUP_DIR="/backup/$(date +%Y%m%d)"
mkdir -p "$BACKUP_DIR"

DATABASES=("users" "orders" "products" "analytics")

for db in "${DATABASES[@]}"; do
    echo "Backing up: $db..."
    pg_dump "$db" | gzip > "$BACKUP_DIR/${db}.sql.gz"
    echo "  Done: $BACKUP_DIR/${db}.sql.gz"
done

echo "All backups complete"

Monitor multiple services

#!/bin/bash
SERVICES=("nginx" "postgresql" "redis")

for service in "${SERVICES[@]}"; do
    if systemctl is-active --quiet "$service" 2>/dev/null; then
        echo "[OK]     $service is running"
    else
        echo "[FAILED] $service is NOT running"
    fi
done

Exercises

Exercise 1: Multiplication table

cat > table.sh << 'EOF'
#!/bin/bash
set -euo pipefail

NUM="${1:-5}"

echo "=== Multiplication Table for $NUM ==="
for ((i=1; i<=10; i++)); do
    printf "%2d x %2d = %3d\n" "$NUM" "$i" "$((NUM * i))"
done
EOF

chmod +x table.sh
./table.sh 7
rm table.sh

Exercise 2: File counter by extension

cat > count-ext.sh << 'EOF'
#!/bin/bash
set -euo pipefail

DIR="${1:-.}"

echo "File types in $DIR:"
find "$DIR" -type f -name "*.*" | sed 's/.*\.//' | sort | uniq -c | sort -rn | while read -r count ext; do
    printf "  .%-10s %d files\n" "$ext" "$count"
done
EOF

chmod +x count-ext.sh
./count-ext.sh /etc
rm count-ext.sh

Exercise 3: Retry pattern

cat > retry.sh << 'EOF'
#!/bin/bash
set -euo pipefail

MAX=5

for ((i=1; i<=MAX; i++)); do
    echo "Attempt $i/$MAX..."

    # Simulate: succeeds on attempt 3
    if ((i >= 3)); then
        echo "Success on attempt $i!"
        exit 0
    fi

    echo "Failed, retrying in 1s..."
    sleep 1
done

echo "All attempts failed" >&2
exit 1
EOF

chmod +x retry.sh && ./retry.sh
rm retry.sh

Key Takeaways

  • for x in list — iterate over items, files, ranges
  • for ((i=0; i<n; i++)) — classic counter loops
  • while [[ condition ]] — repeat while true
  • until [[ condition ]] — repeat until true
  • while IFS= read -r line — read files line by line
  • break exits, continue skips
  • Retry loops are essential for Docker/CI reliability
  • Always quote variables in loops: "$file", "$line"

Next Lesson: Lesson 5: Functions & Modularity →