Bash scripting guide · part 15 of 32
Loops
for over lists, files and ranges, C-style for, while and until, break and continue, retry loops, and redirecting a whole loop's input or output.
Bash has three loop forms: for walks over a list of words, while repeats as long as a command
succeeds, and until repeats as long as it fails. Each body sits between do and done. Like
if, the loop conditions are commands and their exit statuses, not boolean expressions.
for over a list
for name in WORDS; do ...; done assigns each word to name in turn. The words go through the
usual expansions, so they can come from literals, arrays, globs or brace expansion. Leaving out
in WORDS loops over the script's arguments, "$@".
Loop over words, an array and the arguments
for env in dev staging prod; do echo "deploy to $env"; done
hosts=(web01 "web 02")
for h in "${hosts[@]}"; do echo "ping [$h]"; done
set -- one two
for arg; do echo "arg: $arg"; done
deploy to dev deploy to staging deploy to prod ping [web01] ping [web 02] arg: one arg: two
The array loop keeps web 02 as one item because "${hosts[@]}" is quoted. for arg with
no in iterated over the positional parameters set by set --.
Looping over files
Use a glob directly in the for list. Each matching filename is one word, spaces included,
and the names come out sorted. Never loop over $(ls), which splits names on spaces and
mangles unusual characters.
Loop over files with a glob
for f in logs/*.log; do
printf '%-16s %3d lines\n' "$f" "$(wc -l < "$f")"
done
for f in *.txt; do echo "text file: [$f]"; done
logs/app.log 12 lines logs/error.log 2 lines text file: [file with spaces.txt] text file: [notes.txt] text file: [todo.txt]
The glob handled file with spaces.txt correctly. Inside the loop, "$f" is quoted, as every
variable holding a filename should be.
If nothing matches, bash leaves the pattern unchanged, and the loop runs once with the literal
text *.bak as the "filename". Guard with [[ -e $f ]] || continue, or turn on nullglob so a
non-matching glob expands to nothing.
A glob that matches nothing
for f in *.bak; do echo "would delete: $f"; done
shopt -s nullglob
for f in *.bak; do echo "would delete: $f"; done
echo "with nullglob the loop body never ran"
would delete: *.bak with nullglob the loop body never ran
Without nullglob, the loop body ran once with the unexpanded pattern *.bak, which is
how scripts end up running commands on a file called *.bak.
Ranges and C-style for
{1..5} and {01..10..3} are brace expansions: they generate words before anything else
happens, which also means they cannot contain variables. For a range with variable bounds,
use the arithmetic for (( init; condition; step )) loop, or seq.
Fixed ranges, variable ranges and C-style for
echo {1..5} / {01..10..3} / {a..e}
n=3
echo {1..$n}
for (( i = 1; i <= n; i++ )); do printf 'step %d of %d\n' "$i" "$n"; done
for i in $(seq 0 5 15); do printf '%s ' "$i"; done; echo
1 2 3 4 5 / 01 04 07 10 / a b c d e
{1..3}
step 1 of 3
step 2 of 3
step 3 of 3
0 5 10 15 Brace ranges support zero padding and a step. {1..$n} did not expand, because brace
expansion runs before variables are substituted, so it printed literally. The C-style loop
and seq both handle variable bounds.
while and until
while COMMAND; do ...; done runs the body as long as COMMAND exits 0. Any command works:
(( )) for counters, [[ ]] for tests, read for input, pgrep for "while a process is
running". until is the inverse and reads well for waiting.
Count with while, wait with until
n=3
while (( n > 0 )); do echo "T-minus $n"; (( n-- )); done
( sleep 1; touch ready.flag ) &
tries=0
until [[ -e ready.flag ]]; do
(( ++tries )); sleep 0.2
done
echo "ready.flag appeared after $tries checks"
T-minus 3 T-minus 2 T-minus 1 ready.flag appeared after 5 checks
The until loop polled every 0.2 seconds until a background job created the file. The exact
number of checks depends on timing. In real scripts, also cap the number of tries so a file
that never appears does not hang the script.
break and continue
continue skips to the next iteration; break leaves the loop. Both take an optional number
to act on an enclosing loop: break 2 exits two levels at once.
Skip and stop
while IFS= read -r line; do
[[ $line == \#* || -z $line ]] && continue
[[ $line == timeout* ]] && { echo "found: $line"; break; }
echo "setting: $line"
done < data/settings.conf
for dir in data logs src; do
for f in "$dir"/*; do
[[ $f == *.log.1 ]] && { echo "old log in $dir, stopping both loops"; break 2; }
done
echo "$dir checked"
done
setting: host = 0.0.0.0 setting: port = 8080 setting: log_level = info setting: log_file = /var/log/demo/app.log found: timeout = 30 data checked old log in logs, stopping both loops
The first loop skipped comments and blank lines with continue and stopped at timeout.
In the second, break 2 left both the inner and the outer loop as soon as logs/app.log.1
turned up, so src was never checked and logs checked never printed.
Retry loops
A bounded retry is a for over attempts with a break on success. Put the command being
retried in the if so a failure does not trip set -e.
Retry a command with a delay
ok=0
for attempt in 1 2 3; do
if curl -fsS -o /dev/null "http://localhost:8080/status/503"; then
ok=1; break
fi
echo "attempt $attempt failed"
sleep "$attempt"
done
(( ok )) || echo "giving up after $attempt attempts"
curl: (22) The requested URL returned error: 503 attempt 1 failed curl: (22) The requested URL returned error: 503 attempt 2 failed curl: (22) The requested URL returned error: 503 attempt 3 failed giving up after 3 attempts
The test server's /status/503 always fails, so curl -f returns an error each time; -sS
hides the progress meter but still prints the error. The delay grows with each attempt, a
simple backoff. After the loop, attempt keeps its last value and the ok flag says whether
any attempt succeeded.
Redirecting a whole loop
A loop is a compound command, so you can redirect its input or output once, after done,
instead of on every command inside. This is faster and keeps the file open across iterations.
Piping into or out of a loop is also possible, but a pipe runs the loop in a subshell, so
variables set inside are lost afterwards (Reading files line by line shows the fix).
Write and pipe a loop's output
for f in data/*.csv; do
echo "$(head -n 1 "$f") <- $f"
done > headers.txt
cat headers.txt
for n in 5 3 9 1; do echo "$n"; done | sort -n | tr '\n' ' '; echo
date,region,product,units,price <- data/sales.csv id,name,email,dept,joined <- data/users.csv 1 3 5 9
All output of the first loop went to headers.txt with a single redirection. The second loop's
output was piped through sort as one stream.
Infinite loops
while true; do ...; done (or while :) runs until something inside breaks out or the
script is killed. Combine it with sleep for polling, read for interactive menus, and always
give it a clear exit: a break, an exit, or a Traps and signals handler for cleanup on Ctrl-C.
Related
seqPrint a sequence of numbers, with a chosen start, step, separator and format.sleepPause for a given amount of time, in seconds by default, with m, h and d suffixes and fractions allowed.readRead one line from standard input and split it into shell variables.mapfileRead lines from standard input into an indexed array, one line per element (also called readarray).trueDo nothing and exit with status 0 (success).- ArraysIndexed arrays in bash, creating, reading, appending, looping, slicing and removing elements, and using them to build command lines safely.
- Reading files line by lineThe correct
while IFS= read -r lineloop part by part, the missing last line, the pipe-subshell trap, CSV fields, mapfile and stdin-stealing commands. - ConditionalsHow if, elif, &&, || and ! decide using exit statuses, the three test syntaxes, guard clauses, and why
a && b || cis not an if-else. - Brace expansionGenerate lists of words with {a,b,c} and sequences with {1..10} and {01..10..2}, why {1..$n} does not work, and how braces differ from globs.
- GlobbingHow *, ? and [...] match file names, what happens when nothing matches, and the shell options nullglob, failglob, dotglob, globstar and extglob.
- ArithmeticInteger math with $(( )) and (( )), the exit status of (( )) that breaks set -e, leading-zero octal surprises, and what to use for decimals.