The decision to use a bash for loop comes down to how much time you want to spend typing file names. If you've ever found yourself with 200 files all needing the same operation run on them, as I did during a log cleanup task, then using a for loop will save you hours of tedious mouse-clicking. It may seem like nothing initially, but once you see your four-line script running against hundreds of files in seconds, you'll understand why this loop has become an essential part of my workflow.
In fact, the first time I used a for loop was while cleaning up old logs from our production server. I had more than 300 dated log files sitting inside a single directory. My boss asked me to remove everything older than seven days. Doing this manually would have taken nearly an entire afternoon of clicking around. Instead of spending the day click-click-clicking through files, I wrote a simple ten-second script that completed the entire task automatically. I still remember watching the filenames scroll down the terminal as each file was processed in sequence. That experience completely changed the way I approached shell scripting.
This document is for anyone writing Bash scripts. Whether you're new to scripting or you've been doing it for years and simply need reference points for common automation workflows, this guide will walk through the Bash for loop patterns that repeatedly appear in real-world Linux administration and scripting work.
Why Loops Matter Before You Write One:
Think about the last repetitive thing you did at the terminal. Maybe it was renaming a batch of files, checking disk usage across a dozen mount points, or restarting services one by one. Any time you find yourself typing the same command with a slightly different value each time, that is a for loop waiting to be written.
- Processing multiple files without writing individual commands for each one
- Running the same check across a list of servers or directories
- Automating backup, cleanup, or deployment steps that repeat on a schedule
Once you understand how for loops work, a whole category of tedious manual work disappears.
If you are still finding your footing with shell scripts generally, the introduction to bash scripting article covers the foundation you need before loops start making full sense.
The Anatomy of a Bash for Loop
Before writing anything useful, you need to understand the three pieces that every bash for loop is built from. Miss any one of them and the script either throws an error or does nothing at all.
The basic structure looks like this:
LinuxTeck.com
for variable in list
do
# commands here - run once per item
done
# One-liner form (same result)
for variable in item1 item2 item3; do echo $variable; done
Three parts. The for keyword starts the loop. The variable holds the current item on each pass. The list is whatever you want to iterate over filenames, numbers, hostnames, strings. Bash takes each item from the list in order, assigns it to the variable, runs the commands inside do...done, and repeats until the list runs out.
Note:
The variable name inside a for loop is completely up to you. i, file, server, item anything works. Using a descriptive name like $filename instead of $i makes scripts far easier to read six months later when you have forgotten what the script does.
Iterating over Numbers: Ranges, Steps, and the C-Style Loop
Number-based loops come up constantly. Counting iterations, generating test data, running something N times. Bash gives you two clean ways to do it.
The brace expansion method uses {start..end} syntax and is the most readable:
LinuxTeck.com
# Loop from 1 to 5
for i in {1..5}
do
echo "Iteration: $i"
done
# Loop with a step; count by 2s
for i in {0..10..2}
do
echo "Even: $i"
done
# C-style for loop
for (( i=1; i<=5; i++ ))
do
echo "Count: $i"
done
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5
Even: 0
Even: 2
Even: 4
Even: 6
Even: 8
Even: 10
Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
The C-style loop with (( i=1; i<=5; i++ )) gives you explicit control over start, stop, and increment. It is useful when the range is dynamic and you need to calculate it at runtime rather than hard-code it with braces.
Looping over Files, Arrays, and Command Output
This is where bash for loops start doing real work. Instead of a hardcoded list, you feed the loop something dynamic, files in a directory, items from an array, or the output of another command.
Looping over files with a glob pattern is one of the most common patterns in any sysadmin's toolkit:
LinuxTeck.com
# Loop over .log files in current directory
for file in *.log
do
echo "Processing: $file"
done
# Loop over an array
servers=("web01" "web02" "db01" "db02")
for server in "${servers[@]}"
do
echo "Checking server: $server"
done
# Loop over command output
for user in $(cut -d: -f1 /etc/passwd | head -5)
do
echo "User found: $user"
done
Processing: error.log
Processing: system.log
Checking server: web01
Checking server: web02
Checking server: db01
Checking server: db02
User found: root
User found: daemon
User found: bin
User found: sys
User found: sync
One thing worth knowing about arrays: ${servers[@]} expands to all elements as separate items. If you accidentally write ${servers} without the [@], bash gives you only the first element. That is a common source of confusion for anyone looping over arrays for the first time.
Controlling Loop Flow: break, continue, and Nested Loops
A loop that runs unconditionally from start to finish is useful, but real scripts usually need more control. You might want to skip certain items, stop early when a condition is met, or nest one loop inside another for more complex operations.
Here is how break and continue work inside a for loop, followed by a nested loop example:
LinuxTeck.com
# Skip even numbers using continue
for i in {1..8}
do
if (( i % 2 == 0 )); then
continue
fi
echo "Odd: $i"
done
# Stop when we find a specific file
for file in /var/log/*.log
do
[ -e "$file" ] || continue
if [[ "$file" == *"error"* ]]; then
echo "Found error log: $file"
break
fi
echo "Checked: $file"
done
# Nested loop: combine two lists
for env in dev staging prod
do
for service in web api db
do
echo "Deploy $service to $env"
done
done
Odd: 3
Odd: 5
Odd: 7
Found error log: /var/log/error.log
Deploy web to dev
Deploy api to dev
Deploy db to dev
Deploy web to staging
Deploy api to staging
Deploy db to staging
Deploy web to prod
Deploy api to prod
Deploy db to prod
The Mistake That Breaks File Loops (and How to Fix It)
There is one mistake that comes up so consistently with bash for loops and files that it deserves its own section. Almost everyone hits it at least once, usually in production at an inconvenient time.
The mistake involves filenames with spaces. Look at this script:
LinuxTeck.com
# BAD: This breaks on filenames with spaces
for file in $(ls /home/ubuntu/reports/)
do
echo "Processing: $file"
done
# GOOD: Use glob expansion instead
for file in /home/ubuntu/reports/*
do
echo "Processing: $file"
done
# Also GOOD: Use find with while read for safety
find /home/ubuntu/reports/ -type f | while IFS= read -r file
do
echo "Processing: $file"
done
Common Mistake:
Using for file in $(ls) to loop over files is a pattern you will see everywhere, and it breaks the moment any filename contains a space. The shell splits the output of ls on whitespace, so a file named Q1 Report.pdf becomes two separate loop items: Q1 and Report.pdf. Your script then tries to process files that do not exist.
Fix: Use a glob pattern like for file in /path/* instead. Bash handles filename expansion correctly, including spaces. If you need recursive traversal or more control, use find ... | while IFS= read -r file - the IFS= and -r flags preserve spaces and backslashes in filenames.
Real-World Automation: Backup Script with a for Loop
This is where everything comes together. The following script is something you could actually put on a server right now. It loops over a list of directories, creates a timestamped compressed backup of each one, and logs what it did.
This kind of script is typically paired with a cron job to run nightly. If you want to understand how the scheduling side works, the cron command guide walks through the setup in detail.
LinuxTeck.com
# Automated backup script using a for loop
BACKUP_DIR="/var/backups/daily"
LOG_FILE="/var/log/backup.log"
TIMESTAMP=$(date +%Y-%m-%d_%H-%M)
# Directories to back up
SOURCE_DIRS=("/etc" "/home" "/var/www")
# Create backup directory if it does not exist
mkdir -p "$BACKUP_DIR"
for dir in "${SOURCE_DIRS[@]}"
do
if [ ! -d "$dir" ]; then
echo "[SKIP] $dir does not exist" | tee -a "$LOG_FILE"
continue
fi
dirname=$(basename "$dir")
archive="$BACKUP_DIR/${dirname}_${TIMESTAMP}.tar.gz"
tar -czf "$archive" -C "$(dirname "$dir")" "$(basename "$dir")" 2>>"$LOG_FILE"
if [ $? -eq 0 ]; then
echo "[OK] Backed up $dir -> $archive" | tee -a "$LOG_FILE"
else
echo "[FAIL] Backup failed for $dir" | tee -a "$LOG_FILE"
fi
done
echo "Backup run complete: $TIMESTAMP" | tee -a "$LOG_FILE"
[OK] Backed up /home -> /var/backups/daily/home_2026-05-25_02-00.tar.gz
[OK] Backed up /var/www -> /var/backups/daily/www_2026-05-25_02-00.tar.gz
Backup run complete: 2026-05-25_02-00
A few things in this script are worth noting specifically. The -d flag checks that the source directory actually exists before attempting to archive it. The continue statement skips missing directories gracefully instead of letting the script fail. The exit code check with $? tells you whether the tar command actually succeeded. And tee -a sends output to both the screen and the log file at the same time.
Tip:
Be careful with set -e in loops like this one. It causes the script to exit instantly on any minor warning or error, which bypasses the custom error handling you have already built with continue and if [ $? -eq 0 ]. For complex automation loops where you want to skip failures and log them gracefully, handle exit codes manually as shown here rather than enabling global execution halts. For a full breakdown of exit code patterns, the bash exit codes and error handling guide is worth reading alongside this script.
FAQ
Why does my for loop only process the first word in a filename?
Bash splits the loop list on spaces by default. So if you have a file called server report.txt, the loop sees server and report.txt as two separate items. The fix is to use glob patterns (for file in /path/*) instead of command substitution with ls, and always double-quote your variable: "$file" not $file.
Can I use a variable to set the range in {1..N}?
Not directly. Brace expansion in bash happens before variable substitution, so for i in {1..$N} does not work; you will literally get the string {1..$N} as the loop item. Use the C-style form instead: for (( i=1; i<=N; i++ )) - this evaluates arithmetic correctly and works with variables.
What is the difference between ${array[@]} and ${array[*]}?
When unquoted, both behave the same. When double-quoted, "${array[@]}" expands each element as a separate quoted word which is what you almost always want inside a for loop. "${array[*]}" joins all elements into a single string separated by the first character of IFS. Use [@] in loops unless you specifically need the joined form.
My for loop runs fine when I test it manually but skips items in cron. Why?
Cron runs scripts in a minimal environment with a very limited PATH. Glob patterns can also behave differently depending on the working directory. Two things to check: first, use absolute paths for everything inside the loop. Second, make sure the shebang line at the top of your script is present and correct, without #!/bin/bash, cron may run the script with a different shell that handles expansions differently.
How do I loop over lines in a file without splitting on spaces?
Do not use a for loop for this. Use while IFS= read -r line piped from the file. The IFS= prevents leading and trailing whitespace from being stripped, and -r stops backslashes from being interpreted. Example: while IFS= read -r line; do echo "$line"; done < file.txt. For loops split on whitespace by design, which makes them the wrong tool for line-by-line file reading.
Is there a way to run loop iterations in parallel?
Yes. Append an ampersand & after the command inside the loop to send each iteration to the background. Add wait after the loop closes to let all background jobs finish before the script continues. Be careful with this on scripts that write to the same files parallel writes without locking will cause corruption. For anything more complex than simple parallel processing, look into GNU Parallel, which gives you much finer control.
Summary
Now that you have this working, the pattern is the same regardless of what you are iterating over. Files, numbers, arrays, command output a bash for loop handles all of it with the same four-line structure. The real skill is knowing which form to reach for: brace expansion for simple ranges, glob patterns for files, ${array[@]} for arrays, and the C-style form when you need runtime-calculated bounds.
What breaks for loops in practice is almost always one of three things: missing quotes around variables, using $(ls) instead of globs, or forgetting that ${array} and ${array[@]} are not the same. Keep those in mind and most loop problems disappear before they start.
The official GNU Bash Reference on Looping Constructs is worth bookmarking for the edge cases. From here, a natural next step is looking at if/elif/else statements in bash, which combine with loops to handle the conditional logic most real scripts need.
Related Articles
- What Is Bash Scripting in Linux (Part 1 of 34)
- Your First Bash Script: Hello World and Beyond (Part 4 of 34)
- Bash if elif else Statement with Real Examples (Part 10 of 34)
- Bash Script Exit Codes and Error Handling (Part 5 of 34)
- Linux Shell Scripting Command Cheat Sheet
- Automatic Linux Backup Script with Cron
Learn step-by-step how to automate Linux tasks with real-world scripts and practical examples.