Loops & Iteration
Loops & Iteration
Production scripts almost never do one thing once. They deploy to ten servers, rotate thirty log files, validate every line of a config, or restart a service until it becomes healthy. Loops are what transform a one-shot command into a general-purpose automation. This lesson covers the four loop patterns you will reach for every day as a DevOps engineer: the C-style for loop, the list-based for-in loop, the while loop, and reading files line by line — plus the idiomatic glob loop that processes entire directory trees safely.
The for-in Loop: Iterating Over a List
The most common Bash loop iterates over a whitespace-separated list of words. Its form is for variable in list; do ... done. The list can be literal words, a command substitution, a brace expansion, or a glob pattern.
for item in $(command) when output can contain spaces or newlines inside a single logical item. For example, filenames with spaces will be split into separate iterations. Use while IFS= read -r (covered below) or mapfile when the input is newline-delimited.
The while Loop: Condition-Driven Iteration
A while loop runs as long as its condition evaluates to true (exit status zero). It is the right tool when you do not know up front how many iterations you need — waiting for a service to become healthy, polling an API, or draining a queue.
Two details matter here. First, the ! negates the exit status of curl -sf, so the loop continues while the service is not responding. Second, the timeout guard with an explicit exit 1 prevents the script from looping forever if something is truly broken — an essential safety net for CI pipelines and on-call automation.
sleep $(( INTERVAL * 2 ** attempt )). This avoids hammering a degraded service. Libraries like retry (a small shell function you paste into your script) implement this pattern cleanly.
Reading Files Line by Line
A very common DevOps task is to process a list stored in a file: a list of hosts, a manifest of S3 objects, a CSV of usernames. The canonical and safe pattern is while IFS= read -r line. Do not use for line in $(cat file) — it breaks on spaces, tabs, and leading/trailing whitespace.
The redirection < "${HOSTS_FILE}" feeds the file into the while loop's stdin. This is more efficient than piping through cat (which spawns a subprocess) and keeps the loop in the current shell so variable assignments inside the loop remain visible after it ends — a subtle but important difference.
cat file | while read -r line; do VAR=something; done, the loop body runs in a subshell because of the pipe. Any variable you set inside is lost when the loop exits. The while ... done < file redirect avoids this because no pipe is involved.
Glob Loops: Processing Files Safely
When you need to act on every file matching a pattern — all .log files in a directory, every *.yaml config — Bash glob expansion is both safer and faster than parsing ls output. The shell expands the glob before the loop runs, so each filename is a separate, properly quoted word even if it contains spaces.
The shopt -s nullglob line is critical. Without it, if no files match the pattern, Bash passes the literal string /var/log/myapp/*.log as the first (and only) iteration — causing your script to attempt to archive a file that does not exist. With nullglob enabled, a matching set of zero files simply causes the loop body to never execute. Always set it before a glob loop that might match nothing.
Loop Control: break, continue, and Exit Codes
Two built-in keywords modify loop execution. break exits the innermost loop immediately; continue skips the remainder of the current iteration and starts the next one. Both accept an optional integer argument to target outer loops in nested structures.
break, set a flag variable before breaking — PIPELINE_FAILED=1 — then check it after the loop and exit 1 if set. With set -e active this distinction matters: a failed command in a loop with a trailing || true does not abort the script, giving you control over which failures are fatal.
Practical Production Example: Multi-Server Log Rotation
Combining everything in this lesson: a script that reads a host list from a file, iterates over each host, uses a glob loop remotely to archive old logs, and retries failed hosts once before alerting.
This script is production-ready in shape: it reads input safely, skips blank and comment lines, accumulates failures rather than aborting on the first one, and exits non-zero only if any host failed — exactly the behavior a monitoring system or CI pipeline expects.