Bash Loop Constructs

Loop constructs and iteration patterns.

For Loop Styles

# List iteration
for host in vault-01 ise-01 bind-01; do
    echo "Processing $host"
done

# Array iteration (ALWAYS quote!)
hosts=("vault-01" "ise-01" "bind-01")
for host in "${hosts[@]}"; do
    ping -c1 "$host" &>/dev/null && echo "$host: UP"
done

# Glob expansion
for file in /etc/ssl/certs/*.pem; do
    [[ -f "$file" ]] || continue             # Handle no matches
    openssl x509 -in "$file" -noout -subject 2>/dev/null
done

# Range (brace expansion)
for i in {1..10}; do echo "$i"; done
for i in {0..100..10}; do echo "$i"; done    # Step by 10
for letter in {a..z}; do echo "$letter"; done

# C-style (arithmetic)
for ((i=0; i<10; i++)); do
    echo "Index: $i"
done

# C-style with multiple variables
for ((i=0, j=10; i<j; i++, j--)); do
    echo "i=$i, j=$j"
done

# Infinite loop (with break)
for (( ;; )); do
    read -r -p "Continue? [y/n] " answer
    [[ "$answer" == "n" ]] && break
done

While Loops

# Counter-based
count=0
while (( count < 5 )); do
    echo "Count: $count"
    ((count++))
done

# Condition-based
while [[ ! -f /tmp/ready.flag ]]; do
    echo "Waiting for ready flag..."
    sleep 5
done

# Read from file (CORRECT - no subshell)
while IFS= read -r line; do
    echo "Line: $line"
done < /etc/hosts

# Read from command (process substitution)
while IFS= read -r pod; do
    echo "Pod: $pod"
done < <(kubectl get pods -o name)

# Read with multiple fields
while IFS=: read -r user _ uid gid _ home shell; do
    echo "User: $user, UID: $uid, Home: $home"
done < /etc/passwd

# Read with custom delimiter
while IFS=',' read -r name ip role; do
    echo "$name ($ip) - $role"
done < hosts.csv

# Infinite loop with break condition
while true; do
    status=$(curl -s -o /dev/null -w "%{http_code}" https://vault-01:8200/v1/sys/health)
    [[ "$status" == "200" ]] && break
    echo "Vault not ready (HTTP $status), waiting..."
    sleep 5
done
echo "Vault is ready!"

Until Loops

# Opposite of while - runs UNTIL condition is true
count=0
until (( count >= 5 )); do
    echo "Count: $count"
    ((count++))
done

# Wait for service
until systemctl is-active --quiet docker; do
    echo "Waiting for Docker..."
    sleep 2
done

# Wait for port
until nc -z vault-01 8200 2>/dev/null; do
    echo "Waiting for Vault port..."
    sleep 1
done

# Retry pattern
attempts=0
max_attempts=5
until curl -sf https://vault-01:8200/v1/sys/health >/dev/null || (( attempts >= max_attempts )); do
    ((attempts++))
    echo "Attempt $attempts/$max_attempts failed"
    sleep $((attempts * 2))                  # Exponential backoff
done

if (( attempts >= max_attempts )); then
    echo "Failed after $max_attempts attempts"
    exit 1
fi

Loop Control Flow

# break - exit loop entirely
for host in "${hosts[@]}"; do
    if ! ping -c1 -W1 "$host" &>/dev/null; then
        echo "ERROR: $host unreachable, aborting"
        break
    fi
done

# continue - skip to next iteration
for file in /var/log/*.log; do
    [[ ! -r "$file" ]] && continue           # Skip unreadable
    [[ $(stat -c %s "$file") -eq 0 ]] && continue  # Skip empty
    tail -5 "$file"
done

# break N - exit N levels of nesting
for i in {1..3}; do
    for j in {1..3}; do
        echo "i=$i, j=$j"
        [[ $j -eq 2 ]] && break 2            # Exit both loops
    done
done

# continue N - continue at N levels up
for i in {1..3}; do
    for j in {1..3}; do
        [[ $j -eq 2 ]] && continue 2         # Skip to next i
        echo "i=$i, j=$j"
    done
done

# Return from function within loop
check_all_hosts() {
    for host in "$@"; do
        if ! ping -c1 -W1 "$host" &>/dev/null; then
            return 1                         # Early return on first failure
        fi
    done
    return 0
}

The Subshell Trap (CRITICAL)

# WRONG: Pipe creates subshell, variables lost!
count=0
cat /etc/hosts | while read -r line; do
    ((count++))
done
echo "Count: $count"                         # Always 0!

# CORRECT: Process substitution (no pipe)
count=0
while read -r line; do
    ((count++))
done < <(cat /etc/hosts)
echo "Count: $count"                         # Correct count!

# CORRECT: Redirect from file
count=0
while read -r line; do
    ((count++))
done < /etc/hosts
echo "Count: $count"                         # Correct count!

# CORRECT: Here-string
count=0
while read -r line; do
    ((count++))
done <<< "$(cat /etc/hosts)"
echo "Count: $count"                         # Correct count!

# CORRECT: lastpipe option (bash 4.2+)
shopt -s lastpipe
count=0
cat /etc/hosts | while read -r line; do
    ((count++))
done
echo "Count: $count"                         # Now works with pipe!

# Array building - same trap applies
hosts=()
cat hostlist.txt | while read -r h; do
    hosts+=("$h")                            # Modifies subshell's array!
done
echo "${#hosts[@]}"                          # 0!

# CORRECT
hosts=()
while read -r h; do
    hosts+=("$h")
done < hostlist.txt
echo "${#hosts[@]}"                          # Correct!

# CORRECT with mapfile
mapfile -t hosts < hostlist.txt

Parallel Loop Execution

# Background jobs
hosts=("vault-01" "ise-01" "bind-01" "kvm-01" "nas-01")
for host in "${hosts[@]}"; do
    (
        result=$(ssh -o ConnectTimeout=5 "$host" "uptime" 2>/dev/null)
        echo "$host: ${result:-UNREACHABLE}"
    ) &
done
wait                                         # Wait for all background jobs

# Limit concurrent jobs
max_jobs=3
job_count=0
for host in "${hosts[@]}"; do
    (
        ssh "$host" "expensive_operation"
    ) &
    ((job_count++))

    if (( job_count >= max_jobs )); then
        wait -n                              # Wait for ANY one to finish
        ((job_count--))
    fi
done
wait                                         # Wait for remaining

# Using xargs for parallel execution
printf '%s\n' "${hosts[@]}" | xargs -P5 -I{} ssh {} "hostname && uptime"

# Parallel with GNU parallel
printf '%s\n' "${hosts[@]}" | parallel -j5 "ssh {} 'hostname && df -h /'"

# Collect results from parallel jobs
results=()
for host in "${hosts[@]}"; do
    result=$(
        ssh -o ConnectTimeout=5 "$host" "uptime" 2>/dev/null || echo "UNREACHABLE"
    ) &
done

# Wait and collect (using temporary files for complex results)
tmpdir=$(mktemp -d)
for host in "${hosts[@]}"; do
    (
        ssh -o ConnectTimeout=5 "$host" "df -h /" > "$tmpdir/$host" 2>/dev/null
    ) &
done
wait

for host in "${hosts[@]}"; do
    if [[ -s "$tmpdir/$host" ]]; then
        echo "=== $host ==="
        cat "$tmpdir/$host"
    fi
done
rm -rf "$tmpdir"

Infrastructure Loop Patterns

# Health check all infrastructure
declare -A host_status
for host in vault-01 ise-01 bind-01 kvm-01 nas-01 home-dc01; do
    if ping -c1 -W2 "$host" &>/dev/null; then
        host_status["$host"]="UP"
    else
        host_status["$host"]="DOWN"
    fi
done

# Report
for host in "${!host_status[@]}"; do
    printf "%-20s %s\n" "$host" "${host_status[$host]}"
done | sort

# Certificate expiry check
for cert in /etc/ssl/certs/domus-*.pem; do
    [[ -f "$cert" ]] || continue
    expiry=$(openssl x509 -in "$cert" -noout -enddate | cut -d= -f2)
    expiry_epoch=$(date -d "$expiry" +%s)
    now_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    if (( days_left < 30 )); then
        echo "WARNING: $(basename "$cert") expires in $days_left days"
    fi
done

# Rolling restart with health check
services=("nginx" "api-server" "worker")
for service in "${services[@]}"; do
    echo "Restarting $service..."
    systemctl restart "$service"

    # Wait for healthy
    attempts=0
    until systemctl is-active --quiet "$service" || (( attempts >= 10 )); do
        ((attempts++))
        sleep 2
    done

    if ! systemctl is-active --quiet "$service"; then
        echo "FAILED: $service did not start"
        exit 1
    fi
    echo "$service: OK"
done

# Process ISE sessions
while IFS=$'\t' read -r mac user profile; do
    echo "MAC: $mac, User: $user, Profile: $profile"
done < <(netapi ise mnt sessions --format json | jq -r '.[] | [.calling_station_id, .user_name, .selected_azn_profiles] | @tsv')

# Kubernetes pod iteration
while read -r pod namespace status; do
    if [[ "$status" != "Running" ]]; then
        echo "WARNING: $pod in $namespace is $status"
        kubectl describe pod "$pod" -n "$namespace" | tail -20
    fi
done < <(kubectl get pods -A --no-headers | awk '{print $2, $1, $4}')

Interactive Menu Loops

# Select menu
PS3="Choose host: "
select host in vault-01 ise-01 bind-01 "Quit"; do
    case "$host" in
        "Quit") break ;;
        "") echo "Invalid option" ;;
        *) ssh "$host"; break ;;
    esac
done

# Custom menu loop
while true; do
    echo ""
    echo "=== Infrastructure Menu ==="
    echo "1) Check host status"
    echo "2) View logs"
    echo "3) Restart service"
    echo "4) SSH to host"
    echo "q) Quit"
    echo ""
    read -r -p "Choice: " choice

    case "$choice" in
        1) check_status ;;
        2) view_logs ;;
        3) restart_service ;;
        4) ssh_to_host ;;
        q|Q) break ;;
        *) echo "Invalid option" ;;
    esac
done

# Confirmation loop
while true; do
    read -r -p "Delete all pods in namespace 'test'? [yes/no]: " confirm
    case "$confirm" in
        yes) kubectl delete pods -n test --all; break ;;
        no)  echo "Cancelled"; break ;;
        *)   echo "Please type 'yes' or 'no'" ;;
    esac
done

Loop Gotchas

# WRONG: Unquoted glob with no matches
for file in *.txt; do                        # If no .txt files, $file = "*.txt"
    cat "$file"                              # Error: *.txt: No such file
done

# CORRECT: Check if glob matched
for file in *.txt; do
    [[ -e "$file" ]] || continue             # Skip if no match
    cat "$file"
done

# CORRECT: Use nullglob
shopt -s nullglob
for file in *.txt; do                        # Loop body never runs if no matches
    cat "$file"
done
shopt -u nullglob

# WRONG: Modifying array while iterating
for i in "${!arr[@]}"; do
    unset "arr[$i]"                          # Unpredictable behavior!
done

# CORRECT: Build removal list, then remove
to_remove=()
for i in "${!arr[@]}"; do
    [[ "${arr[$i]}" == "bad" ]] && to_remove+=("$i")
done
for i in "${to_remove[@]}"; do
    unset "arr[$i]"
done

# WRONG: Word splitting in for loop
files="file1.txt file2.txt"
for f in $files; do                          # Splits on spaces
    echo "$f"
done

# CORRECT: Use array
files=("file1.txt" "file2.txt")
for f in "${files[@]}"; do
    echo "$f"
done

# WRONG: Read without -r mangles backslashes
while read line; do                          # \n becomes n!
    echo "$line"
done < file.txt

# CORRECT: Always use -r
while read -r line; do
    echo "$line"
done < file.txt

# WRONG: Read loses leading/trailing whitespace
while read -r line; do
    echo "[$line]"                           # "  spaced  " becomes "spaced"
done <<< "  spaced  "

# CORRECT: Set IFS to empty
while IFS= read -r line; do
    echo "[$line]"                           # Preserves whitespace
done <<< "  spaced  "