For loops allow us to repeat tasks and workflow in bash scripts by iteratively processing items in a collection. Whether iterating over a list of files, array of strings, or sequence of numbers – for loops are key to automating repetitive tasks.
In this comprehensive guide, we will explore the intricacies of for loops in bash with insightful real-world examples.
For Loop Syntax
The syntax of a for loop in bash is:
for item in LIST; do
COMMANDS
DONE
Where:
item: Holds the current item from the list on each iterationLIST: A list of items to iterate over. This could be a literal list (array), a file list output using wildcards (*.txt), an integer range, the result of another command, etc.COMMANDS: The commands to execute on each iteration. This block can contain multiple lines of bash commands.DONE: Signals the end of the for loop body.
On every iteration, the item takes on the next value from LIST and the COMMANDS are executed. This continues until all items in LIST have been consumed.
So in a nutshell, the flow is:
- Set
itemto the first value inLIST - Execute
COMMANDS - Set
itemto next value inLIST - Execute
COMMANDS - Repeat steps 3-4 until
LISTis finished.
Under the hood, for loops work by splitting the LIST into individual words, then iteratively taking the first word and executing COMMANDS repeatedly until no more words are left.
With this basic understanding of how for loops are structured, let‘s now look at some common examples.
Iterating Over a Literal List
One simple use case is iterating over a pre-defined list hardcoded into the script:
#!/bin/bash
fruits=("Apple" "Banana" "Orange")
for fruit in ${fruits[@]}; do
echo $fruit
done
Output:
Apple
Banana
Orange
Here ${fruits[@]} expands the fruits array into the full list of items. On each iteration, $fruit takes on the next value, allowing us to process the items individually.
Some key points around iterating lists:
- Use
${array[@]}syntax to fully expand arrays - The list can also be a comma or space separated literal
- Indentation and spacing does not matter in bash
- Remember to use
doneto signify loop end
This method of hardcoding lists works for simple cases, but often we need to construct lists programmatically.
Constructing Lists Programmatically
Instead of manually specifying lists, we can construct them dynamically using command substitutions and wildcards:
List Files in a Directory
for file in *.txt; do
echo $file
done
This will print all files ending in .txt in the current working directory. The *.txt wildcard construct the list of matching files on each iteration.
List Running Processes
for process in $(ps aux); do
echo $process
done
Here we use ps aux and command substitution to generate the list of running processes fed into the for loop.
List Files Modified in Last Day
for file in $(find . -mtime -1); do
echo $file
done
By nesting the find command we construct a list of recently modified files.
As you can see, commands like ls, ps, find and others can output lists of items that we can feed into our for loops.
Iterating Over Integer Ranges
Another common technique is iterating over an integer sequence using brace expansion:
for i in {1..10}; do
echo $i
done
Outputs numbers 1 through 10.
We can also increment by 2, count backwards, set start/stop values:
for i in {10..1}; do
echo $i
done
for i in {1..10..2}; do
echo $i
done
This provides a simple method for repeating numerical operations.
Reading Files Line by Line
A common scripting task is processing a file line-by-line:
while read line; do
echo $line
done < file.txt
- The
readbuiltin reads one line at a time from standard input - We pipe the file into the while loop using
< file.txtto provide lines to read - This prints each line of the file
For larger files, while read is preferred over loading the entire contents at once.
Iterating Arrays
Arrays store multiple elements but need to be expanded into a list for iterating:
fruits=("Apple" "Banana" "Orange")
for fruit in ${fruits[@]}; do
echo $fruit
done
Observe the ${array[@]} expansion syntax. This fully expands the array into a list for the for loop.
Changing Behavior Mid-Iteration
We can dynamically alter loop behavior using conditionals like if/else:
for fruit in ${fruits[@]}; do
if [ "$fruit" = "Banana"]; then
continue
fi
echo "Want: $fruit"
done
Here we skip Banana iterations. Or only process files with .txt extensions:
for file in *; do
if [[ "$file" != *.txt ]]; then
continue
fi
echo "Text file: $file"
done
The continue statement avoids processing the current item and jumps to the next. This alters loop execution flow dynamically.
We can also terminate all iterations early using break:
for process in $(ps aux); do
if [ "$process" = "bash loop.sh" ]; then
echo "Found script process - stopping"
break
fi
echo "Evaluating: $process"
done
Once our script finds its own process running, we break entirely out of evaluating additional processes.
So break and continue introduce logic for fine tuning loops and avoiding unnecessary work.
Infinite Loops
We can intentionally create endless loops using:
while :
do
COMMAND
# Use sleep to prevent overwhelming CPU
sleep 1
done
This infinite while loop executes COMMAND repeatedly until forced to stop by sending SIGINT with Ctrl+C.
Infinite loops are useful for long-running monitoring scripts.
For example, continuously check disk space every 60 seconds:
while :
do
echo "Disk usage:"
df -h
sleep 60
done
We need external intervention to stop this type of endless loop.
Nesting Loops
For more advanced scenarios, loops can be nested:
for i in {1..3}; do
echo "Outer: $i"
for j in {1..3}; do
echo " Inner: $j"
done
done
Output:
Outer: 1
Inner: 1
Inner: 2
Inner: 3
Outer: 2
Inner: 1
Inner: 2
Inner: 3
Outer: 3
Inner: 1
Inner: 2
Inner: 3
The inner loop executes fully for each iteration of the outer loop.
Nesting provides a further dimension of program control flow allowing complex procedural logic. For example, nest for loops to traverse two-dimensional arrays by rows and columns.
Pitfalls To Avoid
While working with for loops, be aware of some common pitfalls:
Forgetting done
Omitting the terminal done causes scripts to stall by entering an infinite loop. Remember the syntax:
for ITEM; do
COMMAND
done ← Don‘t forget terminal done!
Variables Not Available Outside Loop
Variables assigned in loop context vanish outside:
for fruit in ${fruits[@]}; do
count=$((count + 1))
echo "$count. $fruit"
done
# $count no longer accessible
echo $count
Use different variable names inside vs outside of loops to avoid this issue.
No Spaces Around = In Conditional
Watch out for spaces in conditional tests like if/while:
# INCORRECT spacing causes syntax error
for fruit in ${fruits[@]}; do
if [ "$fruit" = "Apple" ]; then
echo "$fruit"
fi
done
# Correct - no spaces around =
for fruit in ${fruits[@]}; do
if [ "$fruit"="Apple" ]; then
echo "$fruit"
fi
done
Omit spaces around = in bash comparisons.
Globals Modified In Loop
Since loops can run commands multiple times, be cautious modifying global state like variables. Repeated mutations can leave state inconsistent post-loop.
Where possible minimize side effects by using locals inside loop bodies.
Following these best practices avoids frustrating syntax issues or logical flaws when working with for loops.
For Loop vs While Loop
The other common loop construct in bash is the while loop:
COUNT=0
while [ $COUNT -lt 10 ]; do
echo $COUNT
COUNT=$((COUNT+1))
done
In contrast to iterating a pre-defined list, while loops based on a condition. The block runs repeatedly while the condition holds true.
So in summary:
forloops iterate over list of itemswhileloops iterate until condition fails
Typically for loops are preferred since we usually have an explicit set of items to process. While loops suit use cases like reading streams that don‘t have definite length.
Other bash loops like until simply invert the while condition, and select allows interactive menus. But for and while are the workhorses for script iteration.
Conclusion
For loops enable us to automate multi-step routines in bash scripts by iterating over predefined lists.
We walked through different item sources from static arrays, to wildcards globs, output commands, integer ranges, and files. Then explored built-in conditional logic with break/continue to further control execution.
By leveraging the programming flow mechanisms provided by for loops, we can repeat tasks over changing inputs without tedious rework. This unlocks huge productivity gains writing reusable bash scripts.
Hopefully the 20+ examples in this guide provide a solid foundation applying for loops with bash for your next scripting project!


