Bash loops are how you automate repetitive tasks, but the real magic is how they interact with the shell’s internal state and process management.
Let’s see a for loop in action:
#!/bin/bash
FILES="file1.txt file2.log file3.conf"
for FILENAME in $FILES
do
echo "Processing $FILENAME..."
if [ -f "$FILENAME" ]; then
wc -l "$FILENAME"
else
echo "$FILENAME not found."
fi
done
When you run this, it iterates through each string in the FILES variable, assigning it to FILENAME one by one. The if statement checks if the file exists, and wc -l counts its lines.
The while loop is driven by a condition:
#!/bin/bash
COUNTER=0
while [ $COUNTER -lt 5 ]; do
echo "Counter is: $COUNTER"
COUNTER=$((COUNTER + 1))
done
This loop continues as long as the condition $COUNTER -lt 5 (counter less than 5) evaluates to true. The ((COUNTER + 1)) is arithmetic expansion, a common way to increment numbers in Bash.
The until loop is the inverse of while:
#!/bin/bash
STATUS=1 # Non-zero indicates an error or "not done"
while [ $STATUS -ne 0 ]; do
echo "Waiting for process to finish..."
# Simulate a check, e.g., checking a PID file or a service status
sleep 2
STATUS=$(ps aux | grep "my_process" | grep -v "grep" | wc -l) # Example check
done
echo "Process is done!"
This until loop keeps going until the condition [ $STATUS -ne 0 ] becomes false (i.e., STATUS is 0). The ps aux | grep "my_process" | grep -v "grep" | wc -l is a common pattern to check if a process is running.
The core problem these loops solve is avoiding manual, repetitive execution of commands. Think about renaming a batch of files, processing every log entry in a directory, or waiting for a service to become available.
Internally, Bash treats each iteration of a loop as a mini-script. The shell parses the commands within the loop body, sets up the environment, executes them, and then, based on the loop’s condition, decides whether to repeat. The exit status of the last command in the loop body is crucial for conditional loops like while and until. For instance, in the while [ $? -eq 0 ] pattern (which we didn’t use above but is common), the loop continues as long as the previous command exited successfully.
The surprising part is how easily you can nest these loops, creating powerful, multi-dimensional processing. Imagine iterating through a list of servers, and for each server, iterating through a list of services to check their status.
#!/bin/bash
SERVERS="server1 server2"
SERVICES="httpd ssh"
for SERVER in $SERVERS; do
echo "Checking services on $SERVER..."
for SERVICE in $SERVICES; do
echo " Checking $SERVICE..."
# In a real scenario, you'd use ssh and systemctl or similar
echo " Status of $SERVICE on $SERVER: OK"
done
done
This demonstrates how for loops can be nested. The inner loop completes entirely for each iteration of the outer loop.
A subtle point is how variable expansion works within loops, especially with spaces in filenames or complex commands. Always quote your variables like "$FILENAME" to prevent word splitting and globbing if the variable contains spaces or special characters. Without quotes, file with spaces.txt would be treated as three separate items: file, with, and spaces.txt.
The break and continue keywords offer fine-grained control. break exits the innermost loop immediately, while continue skips the rest of the current iteration and proceeds to the next. For example, if you’re processing files and encounter an error, you might continue to the next file. If it’s a critical error, you might break out of the entire loop.
The next concept to explore is how to handle command substitution within loops, and the implications of subshells.