If you’ve spent any time writing shell scripts, you already know that repeating the same command over and over by hand is a waste of time. That’s exactly the problem loops solve. Whether I’m renaming a hundred files, checking the status of several servers, or processing lines from a log file, loops are the tool I reach for first. In this guide, I’ll walk you through everything you need to know about loops in Bash — from the absolute basics to the tricks I use in production scripts every day.
What Is a Loop in Bash?
A loop is a control structure that repeats a block of commands until a certain condition is met (or for a fixed number of times). Instead of writing the same line ten times, you write it once and tell Bash how many times, or under what condition, to run it.
Bash gives you three main loop types:
forloops — best when you know the list of items or the number of iterations in advancewhileloops — best when you want to repeat something as long as a condition stays trueuntilloops — the mirror image ofwhile; it repeats as long as a condition stays false
Let’s go through each one in detail.
The for Loop
The for loop is the workhorse of Bash scripting. It iterates over a list of values — this could be a list of numbers, filenames, or words in a string.
Basic Syntax
for variable in list
do
command1
command2
done
Example 1: Looping Over a List of Words
#!/bin/bash
for fruit in apple banana mango grape
do
echo "I like $fruit"
done
Output:
I like apple
I like banana
I like mango
I like grape
Here, fruit is the loop variable. On each iteration, it takes the next value from the list, and the echo command inside the loop body prints it.
Example 2: C-Style for Loop
If you’re coming from a language like C or Java, you’ll recognize this style:
#!/bin/bash
for (( i=1; i<=5; i++ ))
do
echo "Iteration number: $i"
done
Output:
Iteration number: 1
Iteration number: 2
Iteration number: 3
Iteration number: 4
Iteration number: 5
This form has three parts separated by semicolons: initialization (i=1), condition (i<=5), and increment (i++). Bash checks the condition before every iteration, and once it’s false, the loop ends.
Example 3: Looping Through Files
#!/bin/bash
for file in /var/log/*.log
do
echo "Processing $file"
wc -l "$file"
done
This loop expands the glob pattern *.log into a list of matching files and runs the commands against each one. I use this pattern constantly for batch-processing log files or images.
Example 4: Looping Over a Range with Seq
#!/bin/bash
for num in $(seq 1 2 10)
do
echo "Number: $num"
done
Output:
Number: 1
Number: 3
Number: 5
Number: 7
Number: 9
seq 1 2 10 generates odd numbers from 1 to 10 (start, step, end). It’s a handy alternative to the C-style loop when you want a quick range.
The while Loop
A while loop keeps executing as long as its condition evaluates to true.
Basic Syntax
while [ condition ]
do
command1
command2
done
Example: Counting Up
#!/bin/bash
count=1
while [ $count -le 5 ]
do
echo "Count is: $count"
((count++))
done
Output:
Count is: 1
Count is: 2
Count is: 3
Count is: 4
Count is: 5
Every time the loop runs, Bash checks [ $count -le 5 ]. As soon as count becomes 6, the condition fails and the loop exits.
Example: Reading a File Line by Line
This is one of the most practical uses of while in real scripts:
#!/bin/bash
while IFS= read -r line
do
echo "Line: $line"
done < "myfile.txt"
Here IFS= prevents leading/trailing whitespace from being stripped, and -r stops backslashes from being interpreted as escape characters. This is the recommended way to read files line by line in Bash.
Example: Infinite Loop with a Break Condition
#!/bin/bash
while true
do
echo "Running... press Ctrl+C to stop"
sleep 2
done
This runs forever until you manually interrupt it. Infinite loops like this are common in monitoring scripts, where you check something at regular intervals.
The until Loop
until is the opposite of while — it keeps looping until the condition becomes true.
#!/bin/bash
count=1
until [ $count -gt 5 ]
do
echo "Count is: $count"
((count++))
done
Output:
Count is: 1
Count is: 2
Count is: 3
Count is: 4
Count is: 5
I use until less often than while, but it reads more naturally in cases like “wait until the server is up”:
until ping -c1 example.com &>/dev/null
do
echo "Waiting for network..."
sleep 3
done
echo "Network is up!"
Controlling Loops: break and continue
Two keywords give you fine control inside any loop:
breakexits the loop immediatelycontinueskips the rest of the current iteration and moves to the next one
#!/bin/bash
for i in {1..10}
do
if [ $i -eq 5 ]; then
break
fi
echo "Value: $i"
done
This stops the loop as soon as i reaches 5, printing only 1 through 4.
#!/bin/bash
for i in {1..5}
do
if [ $i -eq 3 ]; then
continue
fi
echo "Value: $i"
done
This skips printing 3 but continues on to 4 and 5.
How These Scripts Work Internally
It’s worth understanding what Bash is actually doing behind the scenes:
- Word splitting and globbing — In a
for variable in listloop, Bash first performs word splitting (and filename expansion, if a glob pattern is used) on the list, then assigns each resulting word to the variable in turn. - Condition evaluation — In
while/untilloops, the[ ... ]construct is actually a call to thetestcommand (or[[ ... ]]for the Bash-specific extended test). It returns an exit status of 0 (true) or 1 (false), which the loop uses to decide whether to continue. - Arithmetic context —
((...))is Bash’s arithmetic evaluation context. Expressions likei++orcount-eq 5are evaluated as integer arithmetic, not string comparison. - Subshells and pipes — When you pipe data into a
whileloop (e.g.,cat file | while read line), the loop runs in a subshell. This means variables set inside the loop won’t persist after it ends — a common gotcha I’ll cover below.
Real-World Use Cases
- Batch renaming files:
for file in *.jpeg
do
mv "$file" "${file%.jpeg}.jpg"
done
- Checking multiple servers:
servers=("web1.example.com" "web2.example.com" "db1.example.com")
for server in "${servers[@]}"
do
ping -c1 "$server" &>/dev/null && echo "$server is UP" || echo "$server is DOWN"
done
- Automated backups with retry logic:
attempt=1
until rsync -av /data/ /backup/ || [ $attempt -ge 3 ]
do
echo "Backup failed, retrying... (attempt $attempt)"
((attempt++))
sleep 5
done
- Bulk log analysis:
for log in /var/log/nginx/*.log
do
echo "=== $log ==="
grep "500" "$log" | wc -l
done
Best Practices
- Always quote your variables (
"$file"instead of$file) to avoid word-splitting issues with filenames that have spaces. - Prefer
[[ ]]over[ ]for conditions in Bash scripts — it’s safer and supports pattern matching. - Use
((...))for arithmetic instead ofexpr, which is slower and older-style. - Avoid piping into
while readwhen you need the loop’s variables to persist — use process substitution instead:while read line; do ... done < <(command). - Add a small
sleepin infinite polling loops to avoid hammering the CPU or a remote server.
Security Considerations
- Never loop over unsanitized user input directly in a command without quoting — this can lead to command injection.
- Be careful with
evalinside loops; if loop variables come from untrusted sources,evalcan execute arbitrary code. - When looping over filenames, remember that filenames can contain nearly any character, including newlines. Using
find ... -print0combined withwhile IFS= read -r -d '' fileis much safer than parsing plainlsoutput.
Optimization Tips
- For very large datasets, native Bash loops can be slow compared to tools built for the job, like
awkorsed. If you’re processing millions of lines, consider whether a loop is really the fastest tool. - Avoid spawning unnecessary subprocesses inside a loop (e.g., calling
cat,grep, orwcin every iteration) — batch operations outside the loop when possible. - Use built-in string manipulation (
${variable%pattern},${variable#pattern}) instead of external tools likebasenameinside loops, since built-ins avoid the overhead of spawning a new process.
Troubleshooting Common Loop Issues
- Loop runs once and exits — usually caused by improper quoting or an
IFSissue when reading input. - Variables don’t persist after a piped
whileloop — this happens because the loop runs in a subshell; use process substitution instead. - Infinite loop that never ends — double-check your increment logic and loop condition; a missing
((i++))is a very common cause. - Globbing doesn’t match any files — if no files match a pattern like
*.log, Bash will pass the literal string*.logto the loop unlessnullglobis enabled (shopt -s nullglob).
Frequently Asked Questions
Q: What’s the difference between for, while, and until? A: for iterates over a known list or range, while repeats as long as a condition is true, and until repeats as long as a condition is false.
Q: Can I nest loops in Bash? A: Yes. You can put a for loop inside a while loop or vice versa, just like in any other programming language.
Q: How do I loop through an array in Bash? A: Use for item in "${array[@]}"; do ... done.
Q: Why does my while read loop lose variable values after it ends? A: This happens when the loop is part of a pipeline, which runs it in a subshell. Use input redirection or process substitution instead.
Common Mistakes to Avoid
- Forgetting to quote variables, leading to word-splitting bugs.
- Using
=instead of-eqwhen comparing numbers (that’s for strings). - Not accounting for empty directories when looping over globs.
- Writing infinite loops without a proper exit condition.
Summary
Loops are one of the most fundamental building blocks in Bash scripting. The for loop is ideal when you know your list or range ahead of time, while is perfect for condition-based repetition, and until flips that logic around. Once you’re comfortable combining loops with break, continue, and proper quoting, you’ll be able to automate almost anything from the command line.