Learn Bash Loops the Easy Way to Automate Tasks (Part 20 / 34)

I am knee-deep in a production project. I want my script to take 200+ server log files from a directory, read each one individually, and never require me to touch the keyboard while it runs. At that point, Bash loops stop being about tutorials and start becoming practical.

I have been there too. My first Bash loop had a typo in the variable name. The script ran perfectly, produced no errors, did absolutely nothing, and exited with a status code of 0. No error messages. No output. Just silence. That single experience forced me to pay attention to every detail that matters, which is exactly what this article covers.

If you are new to shell scripting or simply need a solid reference for the three different types of Bash loops, this guide covers both. By the end of this article, you will be writing loops that actually perform useful tasks in production environments.

Real Situation:

You have a directory full of config files. You need to back each one up before a deployment. A bash loop handles that in six lines. Without it, you are doing it manually, file by file, hoping you do not miss one.

  • Log rotation scripts that run nightly via cron
  • Batch file renaming across deployment directories
  • Health checks that ping every server in a list

If repetition is involved, a loop belongs there.

If you are just getting started with scripting, check out the bash script hello world guide first to make sure your environment is set up correctly before diving into loops.

#01

The Three Loop Types and When to Use Each

Bash gives you three loops: for, while, and until. They are not interchangeable, and using the wrong one makes your script harder to read and maintain.

The for loop is for when you know your list upfront. A list of files, a range of numbers, an array of server names. The while loop is for when you keep going as long as a condition stays true. The until loop is the inverse of while: keep going until the condition becomes true. In practice, until shows up less often but it is genuinely useful for polling and wait scenarios.

Note:

All three loops support break and continue. Use break to exit the loop entirely. Use continue to skip the current iteration and move to the next one. You will use both more than you expect.

#02

The for Loop - Your Everyday Bash Loops Workhorse

The for loop covers most of what you need day to day. Here is the basic pattern, then the variations that actually matter.

Looping over a simple list:

bash
LinuxTeck.com
#!/bin/bash
# Loop through a list of servers
for SERVER in web01 web02 web03
do
echo "Checking: $SERVER"
done
OUTPUT
Checking: web01
Checking: web02
Checking: web03

The C-style syntax is useful when you need a counter. This is the same pattern you see in languages like C or Java, and it works exactly as you would expect in bash:

bash
LinuxTeck.com
#!/bin/bash
# C-style counter loop
for ((i=1; i<=5; i++))
do
echo "Iteration: $i"
done
OUTPUT
Iteration: 1
Iteration: 2
Iteration: 3
Iteration: 4
Iteration: 5

Looping over an array is just as clean. Declare the array, then loop with "${servers[@]}". The quotes around it matter when any element has a space in the name:

bash
LinuxTeck.com
#!/bin/bash
# Loop through an array
servers=("web01" "web02" "db01" "cache01")

for SERVER in "${servers[@]}"
do
echo "Pinging: $SERVER"
done

OUTPUT
Pinging: web01
Pinging: web02
Pinging: db01
Pinging: cache01
#03

The while Loop - Writing Bash Loops With Conditions

Use while when you do not know in advance how many times you need to loop. You keep going as long as something is true. A counter that has not hit its limit. A file that still has lines. A process that is still running.

Here is a straightforward counter example first, then the more useful file-reading pattern:

bash
LinuxTeck.com
#!/bin/bash
# Counter-based while loop
COUNT=1

while [[ $COUNT -le 5 ]]
do
echo "Step $COUNT of 5"
((COUNT++))
done

echo "Done."

OUTPUT
Step 1 of 5
Step 2 of 5
Step 3 of 5
Step 4 of 5
Step 5 of 5
Done.

Reading a file line by line is where while read really shines. This is the standard production pattern. The -r flag stops bash from treating backslashes as escape characters, which matters the moment your file contains paths or regex patterns. Always use it:

bash
LinuxTeck.com
#!/bin/bash
# Read a file line by line safely
INFILE="/etc/hosts"

while IFS= read -r LINE
do
echo "Line: $LINE"
done < "$INFILE"

OUTPUT
Line: 127.0.0.1 localhost
Line: 127.0.1.1 myserver
Line: # The following lines are for IPv6

The IFS= prefix on the read command preserves leading and trailing whitespace on each line. Without it, indented lines in config files can lose their formatting. For log parsing and config processing, that distinction matters.

#04

The until Loop and break/continue in Real Scripts

The until loop is less common but genuinely useful for waiting on something. Deployment scripts that need to wait for a service to come up, health checks that keep retrying until they get a 200, that kind of thing.

bash
LinuxTeck.com
#!/bin/bash
# Wait until a service is available
RETRIES=0

until curl -s http://localhost:8080/health > /dev/null 2>&1
do
echo "Service not ready. Retry $RETRIES..."
((RETRIES++))
if [[ $RETRIES -ge 10 ]]; then
echo "Giving up after 10 retries."
exit 1
fi
sleep 5
done

echo "Service is up."

OUTPUT
Service not ready. Retry 0...
Service not ready. Retry 1...
Service is up.

Using break inside any loop exits it early. continue skips the current iteration and moves to the next. Both are useful when filtering inside a loop:

bash
LinuxTeck.com
#!/bin/bash
# Skip hidden files, stop at first large file
for FILE in /var/log/*
do
BASENAME=$(basename "$FILE")

# Skip dotfiles
if [[ "$BASENAME" == .* ]]; then
continue
fi

# Skip non-regular files (dirs, symlinks)
if [[ ! -f "$FILE" ]]; then
continue
fi

SIZE=$(stat -c%s "$FILE" 2>/dev/null)

if [[ -n "$SIZE" && $SIZE -gt 10485760 ]]; then
echo "Large file found: $FILE ($SIZE bytes)"
break
fi

echo "OK: $FILE"
done

OUTPUT
OK: /var/log/auth.log
OK: /var/log/dpkg.log
Large file found: /var/log/syslog (15728640 bytes)
#05

The Mistake That Makes Your Loop Do Nothing

There is a common mistake that trips up people moving from reading about loops to actually writing them. The script runs, exits cleanly, and produces zero output. No error message. Just silence.

Common Mistake:

Using for FILE in $(ls /some/dir/*.log) looks reasonable but breaks badly when filenames contain spaces. The ls output is split by the shell on spaces, so a file named error log.txt becomes two tokens: error and log.txt. Both fail silently or process the wrong paths.

Fix: Use glob patterns directly. for FILE in /some/dir/*.log handles spaces in filenames correctly and is faster because it does not spawn a subprocess.

Here is the wrong version and the corrected version side by side so you can see exactly what changes:

bash
LinuxTeck.com
#!/bin/bash

# WRONG - breaks on filenames with spaces
for FILE in $(ls /var/log/*.log)
do
echo "Processing: $FILE"
done

# CORRECT - use glob directly
for FILE in /var/log/*.log
do
if [[ -f "$FILE" ]]; then
echo "Processing: $FILE"
fi
done

OUTPUT
Processing: /var/log/auth.log
Processing: /var/log/kern.log
Processing: /var/log/syslog
#06

A Real-World Automation Script You Can Use Today

Theory is one thing. Here is something you can actually drop into a cron job. This script reads a list of directories, checks if a backup file exists for today, and creates one if it does not. The kind of thing that runs quietly every night and saves you from the conversation you never want to have about missing backups.

For a deeper look at scheduling, the cron command guide on LinuxTeck covers the full syntax for scheduling scripts like this one.

bash
LinuxTeck.com
#!/bin/bash
# Daily backup check for a list of directories

BACKUP_ROOT="/backups"
TODAY=$(date +%Y-%m-%d)
DIRS=("/etc" "/var/www/html" "/home/deploy")

for DIR in "${DIRS[@]}"
do
# Strip trailing slash, replace slashes with underscores, remove leading underscore
LABEL=$(echo "$DIR" | sed 's/\/$//' | tr "/" "_" | sed "s/^_//")
DEST="${BACKUP_ROOT}/${LABEL}_${TODAY}.tar.gz"

if [[ -f "$DEST" ]]; then
echo "Already backed up: $DIR"
continue
fi

echo "Backing up $DIR to $DEST..."
tar -czf "$DEST" "$DIR"

if [[ $? -eq 0 ]]; then
echo "Done: $DEST"
else
echo "ERROR: Failed to back up $DIR"
fi
done

OUTPUT
Backing up /etc to /backups/etc_2026-05-28.tar.gz...
Done: /backups/etc_2026-05-28.tar.gz
Backing up /var/www/html to /backups/var_www_html_2026-05-28.tar.gz...
Done: /backups/var_www_html_2026-05-28.tar.gz
Already backed up: /home/deploy

This is the kind of script you can drop straight into cron to run every night. Pair it with a log redirect and you have a lightweight backup audit trail. Check the automatic Linux backup script guide for a more complete version with retention and remote copy support.

WHY

What Loops Actually Unlock in Your Workflow

Writing loops makes your scripts far more powerful. They do not just make scripts more complex, they also allow you to automate tasks in ways that would otherwise require repetitive manual work. Instead of opening a file manager and clicking through directories to locate files, you can write six lines of code. If you need to run a command 50 times with slightly different arguments, you do not need to execute it manually each time. You can simply create a list and use a loop to process each item automatically.

You will be surprised how often loops fit naturally into your workflow once you start recognizing repetitive tasks. Any task that needs to be performed more than twice is usually a good candidate for a loop. Multi-server deployments are another common example where loops become extremely useful. Tasks such as log rotation, user provisioning, health checks, SSL renewal notifications, and distributing configuration files across environments are all ideal use cases for loops. The Bash Scripting Automation Guide for 2026 provides a broader overview of how these patterns connect to real DevOps workflows.

Where most people run into problems is writing loops without proper error handling. If a loop processes 50 files and silently fails on file number 12, you may not notice until something downstream breaks. In many cases, that is worse than not using a loop at all. Because of this, it is important to include proper error checking by monitoring $? or enabling set -e at the beginning of scripts where a single failure should stop execution immediately. For a deeper explanation of handling script failures correctly, see the Bash Exit Codes and Error Handling article.

Debugging loops is also worth knowing before you need it. Run your script with bash -x script.sh to see every command as it executes, with variables expanded. When a loop is iterating over the wrong things or skipping files you expect it to catch, that trace output will show you exactly where the logic breaks down. The official GNU Bash manual is a solid reference for the full loop syntax and options when you need to go deeper than examples can take you.

FAQ

Questions I Get Asked About This All the Time

Why does my loop process the wrong files when filenames have spaces?

Word splitting. When you use $(ls) or unquoted variables, bash splits on spaces, so a file named my report.log becomes two separate tokens. Use glob patterns directly (for FILE in /path/*.log) and always wrap $FILE in double quotes inside the loop body. That solves it.

My while loop runs forever. How do I figure out what is wrong?

The condition never became false. Either the counter variable is not being incremented, or the condition logic is inverted. Run bash -x yourscript.sh and watch the condition get evaluated each iteration. You will see the variable value right there in the trace output. Also double check whether you used -le when you meant -ge, or vice versa.

What is the difference between [ ] and [[ ]] in loop conditions?

[[ ]] is the bash-specific extended test command. It handles unquoted variables more safely, supports pattern matching with ==, and does not break on empty variables. [ ] is POSIX-compatible but more fragile. For bash scripts, use [[ ]]. For scripts that need to run under /bin/sh, use [ ].

Why does read -r matter when I am just reading a text file?

Without -r, bash interprets backslashes inside the line as escape characters. So a line containing a Windows-style path like C:\new\thing would have the \n and \t silently swapped for newline and tab characters. The -r flag makes read treat backslashes as literal characters, which is almost always what you want.

Can I nest loops inside each other in bash?

Yes, and it works exactly as you expect. A for loop inside a while loop, a for inside another for. Each loop gets its own variables. Just watch indentation carefully so the logic stays readable. break 2 exits two levels of nesting at once if you need that.

How do I loop through command output, like the result of grep or find?

Pipe the output into a while read loop: find /some/dir -name "*.log" | while IFS= read -r FILE; do echo "$FILE"; done. This handles spaces in filenames correctly and processes output one line at a time without loading everything into memory. For find specifically, you can also use find ... -exec or find ... -print0 | xargs -0 for more complex operations.

END

Summary

Now that you have the three loop types working, you can start replacing anything repetitive in your workflow with a script. The bash loop is usually the first thing that turns a one-off command into something reusable.

The next natural step is combining loops with conditionals and functions to build scripts that make real decisions. The bash if-elif-else guide covers that transition well. From there, look at how to handle errors cleanly so your loops do not silently fail in production.

Related Articles

LinuxTeck - A Complete Linux Learning Blog
Learn step-by-step how to automate Linux tasks with real-world scripts and practical examples.

About Sharon J

Sharon J is a Linux System Administrator with strong expertise in server and system management. She turns real-world experience into practical Linux guides on Linux Teck.

View all posts by Sharon J →

Leave a Reply

Your email address will not be published.

L