Shell Scripting Loops: For, While, Until & Control Flow
Loops are the engine of automation. They let you process thousands of files, monitor systems continuously, and repeat tasks without writing the same code over and over. Whether you're renaming files in bulk, parsing logs, or deploying to multiple servers, mastering loops is what separates beginner scripts from production-grade automation.
In this guide, we'll cover every type of loop Bash offers — for, while, and until — along with loop control statements that let you fine-tune execution flow.
For Loop
The for loop is the most versatile loop in shell scripting. It iterates over a list of items, executing the loop body once for each item.
Iterating Over a List
1#!/bin/bash 2 3for i in 1 2 3 4 5; do 4 echo "Iteration: $i" 5done
Brace Expansion Ranges
Instead of typing each number, use Bash brace expansion:
1#!/bin/bash 2 3# Count from 1 to 5 4for i in {1..5}; do 5 echo "Number: $i" 6done 7 8# Count with a step (0, 2, 4, 6, 8, 10) 9for i in {0..10..2}; do 10 echo "Even number: $i" 11done
Note: Brace expansion
{start..end..step}requires Bash 4.0+. On macOS (which ships with Bash 3.2), use the C-style loop instead.
C-Style For Loop
When you need precise control over initialization, condition, and increment, use the C-style syntax with double parentheses:
1#!/bin/bash 2 3for ((i=1; i<=5; i++)); do 4 echo "Count: $i" 5done
This is especially useful for:
- Reverse counting
- Custom step sizes
- Multiple loop variables
1#!/bin/bash 2 3# Reverse countdown 4for ((i=5; i>=1; i--)); do 5 echo "$i..." 6 sleep 1 7done 8echo "Launch!" 9 10# Multiple variables 11for ((i=0, j=10; i<=10; i++, j--)); do 12 echo "i=$i, j=$j" 13done
Looping Over Files
One of the most common use cases — process files matching a pattern:
1#!/bin/bash 2 3for file in *.txt; do 4 # Check if any files matched (prevents error if no .txt files exist) 5 if [ -f "$file" ]; then 6 echo "Processing: $file" 7 wc -l "$file" 8 fi 9done
Always quote the file variable (
"$file"). Filenames with spaces will break your script otherwise.
Looping Over Arrays
1#!/bin/bash 2 3fruits=("apple" "banana" "cherry" "date") 4 5for fruit in "${fruits[@]}"; do 6 echo "I like $fruit" 7done 8 9# With index 10for i in "${!fruits[@]}"; do 11 echo "$i: ${fruits[$i]}" 12done
Looping Over Command Output
Use command substitution to iterate over the output of another command:
1#!/bin/bash 2 3# List all users from /etc/passwd 4for user in $(cut -d: -f1 /etc/passwd); do 5 echo "User: $user" 6done 7 8# Safer alternative: use a while loop for command output 9# (see While Loop section below)
For Loop with seq
For compatibility with older Bash versions or when you need dynamic ranges:
1#!/bin/bash 2 3start=1 4end=10 5 6for i in $(seq $start $end); do 7 echo "Sequence: $i" 8done 9 10# With step 11for i in $(seq 0 2 10); do 12 echo "Step: $i" 13done
While Loop
The while loop executes as long as a condition remains true. It's ideal when you don't know in advance how many iterations you need.
Counter-Based While Loop
1#!/bin/bash 2 3counter=1 4while [ "$counter" -le 5 ]; do 5 echo "Counter: $counter" 6 ((counter++)) 7done
Don't forget to increment the counter! An infinite loop will hang your terminal.
Reading Files Line by Line
This is the most robust way to process files in shell scripts:
1#!/bin/bash 2 3while read -r line; do 4 echo "Read: $line" 5done < input.txt
Why read -r? The -r flag prevents backslash escapes from being interpreted, which avoids unexpected behavior when reading data containing backslashes.
Reading Files with Line Numbers
1#!/bin/bash 2 3line_num=1 4while read -r line; do 5 echo "$line_num: $line" 6 ((line_num++)) 7done < /etc/passwd
Interactive Input Loop
Create a menu that keeps running until the user chooses to exit:
1#!/bin/bash 2 3while true; do 4 echo "" 5 echo "=== Main Menu ===" 6 echo "1) Show date" 7 echo "2) Show uptime" 8 echo "3) Exit" 9 read -p "Choose an option: " choice 10 11 case $choice in 12 1) date ;; 13 2) uptime ;; 14 3) echo "Goodbye!"; break ;; 15 *) echo "Invalid option" ;; 16 esac 17done
Processing Command Output with While
1#!/bin/bash 2 3# Monitor running processes 4ps -l | while read -r line; do 5 echo "Process: $line" 6done
Warning: Piping into a while loop creates a subshell. Variables modified inside the loop won't persist in the parent shell. Use process substitution
< <(command)to avoid this:
1#!/bin/bash 2 3total=0 4while read -r size _; do 5 ((total += size)) 6done < <(du -b /var/log/*.log 2>/dev/null) 7 8echo "Total log size: $total bytes"
Until Loop
The until loop is the opposite of while — it runs until a condition becomes true. It's less commonly used but can make certain logic more readable.
1#!/bin/bash 2 3count=1 4until [ "$count" -gt 5 ]; do 5 echo "Count: $count" 6 ((count++)) 7done
Practical Example: Wait for a Service
1#!/bin/bash 2 3# Wait until a web service is available 4until curl -s http://localhost:8080/health > /dev/null; do 5 echo "Waiting for service..." 6 sleep 2 7done 8 9echo "Service is up!"
Wait for File Creation
1#!/bin/bash 2 3echo "Waiting for /tmp/ready.flag..." 4until [ -f /tmp/ready.flag ]; do 5 sleep 1 6done 7echo "Flag detected. Proceeding..."
Loop Control Statements
Sometimes you need to skip an iteration or exit a loop early. Bash provides break, continue, and select for fine-grained control.
break — Exit the Loop Early
1#!/bin/bash 2 3for i in {1..10}; do 4 if [ "$i" -eq 5 ]; then 5 echo "Breaking at $i" 6 break 7 fi 8 echo "Processing: $i" 9done
Output:
Processing: 1
Processing: 2
Processing: 3
Processing: 4
Breaking at 5
continue — Skip to Next Iteration
1#!/bin/bash 2 3for i in {1..10}; do 4 if [ $((i % 2)) -eq 0 ]; then 5 continue # Skip even numbers 6 fi 7 echo "Odd number: $i" 8done
Output:
Odd number: 1
Odd number: 3
Odd number: 5
Odd number: 7
Odd number: 9
break with Nested Loops
Use break N to break out of N levels of nested loops:
1#!/bin/bash 2 3for i in {1..3}; do 4 for j in {1..3}; do 5 if [ "$i" -eq 2 ] && [ "$j" -eq 2 ]; then 6 echo "Breaking both loops" 7 break 2 8 fi 9 echo "i=$i, j=$j" 10 done 11done
select — Interactive Menu Loop
The select command creates an interactive numbered menu automatically:
1#!/bin/bash 2 3PS3="Select an action: " # Prompt string 4 5select choice in Start Stop Restart Exit; do 6 case $choice in 7 Start) echo "Starting service..."; break ;; 8 Stop) echo "Stopping service..."; break ;; 9 Restart) echo "Restarting service..."; break ;; 10 Exit) echo "Exiting."; exit 0 ;; 11 *) echo "Invalid option: $REPLY" ;; 12 esac 13done
Output:
1) Start
2) Stop
3) Restart
4) Exit
Select an action: 1
Starting service...
The select loop automatically:
- Numbers the options
- Re-prompts on invalid input
- Stores the user's numeric choice in
$REPLY - Stores the selected value in the loop variable
Real-World Examples
Bulk File Processor
1#!/bin/bash 2 3# Process all .log files: compress if > 10MB, delete if older than 30 days 4LOG_DIR="/var/log/myapp" 5MAX_SIZE=$((10 * 1024 * 1024)) # 10MB in bytes 6 7for file in "$LOG_DIR"/*.log; do 8 [ -f "$file" ] || continue # Skip if not a regular file 9 10 file_size=$(stat -f%z "$file" 2>/dev/null || stat -c%s "$file") 11 filename=$(basename "$file") 12 13 echo "Checking: $filename (${file_size} bytes)" 14 15 # Compress large files 16 if [ "$file_size" -gt "$MAX_SIZE" ]; then 17 gzip -c "$file" > "$file.gz" 18 > "$file" # Truncate original 19 echo " → Compressed to $filename.gz" 20 fi 21 22 # Delete old compressed files 23 if [ -f "$file.gz" ]; then 24 file_age=$(( ($(date +%s) - $(stat -c%Y "$file.gz" 2>/dev/null || stat -f%m "$file.gz")) / 86400 )) 25 if [ "$file_age" -gt 30 ]; then 26 rm "$file.gz" 27 echo " → Deleted old archive: $filename.gz" 28 fi 29 fi 30done
Server Health Monitor
1#!/bin/bash 2 3# Monitor system resources every 5 seconds until interrupted 4echo "Monitoring started. Press Ctrl+C to stop." 5echo "----------------------------------------" 6 7while true; do 8 timestamp=$(date '+%Y-%m-%d %H:%M:%S') 9 cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}') 10 mem_available=$(free -m | awk 'NR==2{printf "%.2f", $7*100/$2}') 11 disk_usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%') 12 13 printf "[%s] CPU idle: %s%% | Mem free: %s%% | Disk used: %s%%\n" \ 14 "$timestamp" "$cpu_idle" "$mem_available" "$disk_usage" 15 16 # Alert if disk usage exceeds 90% 17 if [ "$disk_usage" -gt 90 ]; then 18 echo "⚠️ ALERT: Disk usage critical (${disk_usage}%)!" 19 fi 20 21 sleep 5 22done
User Creation from CSV
1#!/bin/bash 2 3CSV_FILE="users.csv" 4 5if [ ! -f "$CSV_FILE" ]; then 6 echo "Error: $CSV_FILE not found." 7 exit 1 8fi 9 10# Skip header and process each line 11tail -n +2 "$CSV_FILE" | while IFS=',' read -r username fullname role; do 12 # Skip empty lines 13 [ -z "$username" ] && continue 14 15 # Skip if user already exists 16 if id "$username" &>/dev/null; then 17 echo "Skipping existing user: $username" 18 continue 19 fi 20 21 echo "Creating user: $username ($fullname) - Role: $role" 22 useradd -c "$fullname" -m "$username" 23 24 if [ $? -eq 0 ]; then 25 echo " ✓ Created successfully" 26 # Set role-based groups 27 case "$role" in 28 admin) usermod -aG sudo "$username" ;; 29 dev) usermod -aG developers "$username" ;; 30 esac 31 else 32 echo " ✗ Failed to create user" 33 fi 34done
Best Practices for Loops
- Always quote loop variables —
for f in *.txt; do cat "$f"; done - Use
read -rwhen reading files to handle backslashes correctly - Check if files exist before processing in
for file in *.extloops - Avoid parsing
ls— use glob patterns orfindinstead - Prefer
while readoverforfor command output — it handles spaces correctly - Remember subshell behavior — pipes create subshells; use process substitution to preserve variables
- Set a timeout or max iterations on infinite loops to prevent runaway scripts
- Use
breakandcontinuewisely — they make loops cleaner than deep nesting
Quick Reference
1# For loops 2for i in 1 2 3; do ...; done 3for i in {1..10}; do ...; done 4for i in {0..10..2}; do ...; done 5for ((i=0; i<10; i++)); do ...; done 6for file in *.txt; do ...; done 7for item in "${array[@]}"; do ...; done 8 9# While loop 10while [ "$x" -lt 10 ]; do ...; done 11while read -r line; do ...; done < file 12while true; do ...; done 13 14# Until loop 15until [ "$x" -gt 10 ]; do ...; done 16 17# Loop control 18break # Exit loop 19break 2 # Exit 2 nested loops 20continue # Skip to next iteration 21 22# Interactive menu 23select opt in A B C; do ...; done
Loops are where shell scripting becomes truly powerful. Once you can iterate over files, process data line by line, and control execution flow with precision, you can automate virtually any repetitive task on your system.
Next up: Functions and modular scripting — how to write reusable, maintainable code blocks.