Loops (for, while, until)
A loop lets your script repeat the same set of commands many times without you writing them out by hand. This is one of the biggest reasons to script at all: instead of restarting ten services one by one, you write the restart once and let a loop run it ten times. In Bash (the Bourne Again SHell, the default command-line shell on Ubuntu) you have three loop types — for, while, and until — and each fits a different job. By the end of this page you will be able to loop over files in a directory, read a log file line by line, and wait for a condition to become true.
The for loop: repeat over a list
A for loop walks through a list of items one at a time. On each pass (called an iteration) it puts the current item into a variable you name, then runs the body of the loop. Use it when you already know the set of things you want to process — a fixed list of words, a glob of files, or a range of numbers.
#!/usr/bin/env bash
for service in nginx postgresql ssh; do
echo "Checking $service..."
systemctl is-active "$service"
done
Output:
Checking nginx...
active
Checking postgresql...
active
Checking ssh...
active
The structure is for VARIABLE in LIST; do ... done. systemctl is the tool that controls services (units managed by systemd, the program that starts and supervises everything on a modern Ubuntu server). Here is-active simply prints whether each service is running.
Looping over a range of numbers
To repeat a fixed number of times, use a brace expansion range like {1..5}. Bash expands this to 1 2 3 4 5 before the loop runs.
for i in {1..5}; do
echo "Attempt $i"
done
You can also add a step: {0..20..5} gives 0 5 10 15 20. For a count whose end is stored in a variable, use the C-style form instead, because brace expansion does not expand variables:
count=3
for ((i = 1; i <= count; i++)); do
echo "Loop number $i"
done
Avoid the old habit of writing
for f in $(ls). Filenames can contain spaces, andlsoutput will split them into broken pieces. Always loop over a glob (for f in *.log) instead, which Bash splits correctly.
Looping over files in a directory
A glob is a pattern like *.conf that Bash expands into the matching filenames. This is the safe, idiomatic way to process files. Here we check every site config in the Nginx directory (Nginx is a popular web server and reverse proxy — a server that sits in front of your app and forwards requests to it).
#!/usr/bin/env bash
for conf in /etc/nginx/sites-available/*; do
echo "Found config: $conf"
done
Output:
Found config: /etc/nginx/sites-available/default
Found config: /etc/nginx/sites-available/myapp.conf
One gotcha: if no files match, the glob stays as the literal text *. Guard against that by checking the path exists with [ -e "$conf" ], or enable shopt -s nullglob at the top of your script so an unmatched glob expands to nothing.
The while loop: repeat as long as a condition holds
A while loop keeps running its body while a test command succeeds (returns exit code 0, the convention for “success” on Linux). Use it when you do not know in advance how many iterations you need — such as polling until a server comes up, or reading until a file ends.
#!/usr/bin/env bash
attempts=0
while ! curl -fsS http://localhost:8080/health > /dev/null; do
attempts=$((attempts + 1))
echo "Waiting for app... (try $attempts)"
sleep 2
done
echo "App is up after $attempts tries"
curl is a command-line tool for making HTTP requests. The ! flips the result, so the loop continues while the health check is failing. sleep 2 pauses two seconds between checks so you do not hammer the server.
Reading a file line by line: the while read -r pattern
The single most common while use in DevOps is reading a file one line at a time. The idiom is while IFS= read -r line; do ... done < file:
#!/usr/bin/env bash
while IFS= read -r line; do
echo "LOG: $line"
done < /var/log/nginx/access.log
Output:
LOG: 203.0.113.4 - - [15/Jun/2026:09:01:22] "GET / HTTP/1.1" 200
LOG: 203.0.113.7 - - [15/Jun/2026:09:01:25] "GET /api HTTP/1.1" 404
Two details make this correct and worth memorising:
read -rtellsreadnot to treat backslashes as escape characters, so paths and log data come through exactly as written.IFS=(clearing the Internal Field Separator) stopsreadfrom trimming leading and trailing whitespace, preserving each line as-is.
The < /var/log/... part redirects the file into the loop’s input. You can also pipe a command in — for example, process the output of journalctl (the tool that reads systemd’s logs):
journalctl -u nginx --no-pager | while IFS= read -r line; do
echo "$line" | grep -i error
done
The until loop: repeat until a condition becomes true
until is the mirror image of while: it runs until its test succeeds, meaning it loops while the test is failing. Some people find it reads more naturally for “wait for X” tasks.
#!/usr/bin/env bash
until systemctl is-active --quiet postgresql; do
echo "PostgreSQL not ready yet, waiting..."
sleep 1
done
echo "PostgreSQL is active"
--quiet suppresses the printed status so only the exit code is used. This loop exits the moment PostgreSQL (a relational database) reports active.
When to use which
| Loop | Best for | Stops when |
|---|---|---|
for | A known list, glob, or range | The list is exhausted |
while | Reading files, polling, unknown count | The test command fails |
until | Waiting for a condition to come true | The test command succeeds |
Best Practices
- Always quote your loop variable (
"$file") so filenames with spaces do not break. - Use globs (
*.log) instead of parsinglsoutput to iterate over files. - Use
while IFS= read -r linefor reading files — it preserves whitespace and backslashes correctly. - Add
sleepinside pollingwhile/untilloops so you do not spin the CPU at 100 percent. - Cap retry loops with a counter and a maximum so a script cannot hang forever waiting on a dead service.
- Use
breakto exit a loop early andcontinueto skip to the next iteration when a condition is met. - Enable
shopt -s nullglobwhen a glob might match nothing, so the loop body does not run on a literal*.