Bash for Loop: Syntax and Examples for Server Admins

How the Bash for loop works: lists, ranges, C-style counters, files, arrays and break/continue, with tested examples and the mistakes to avoid.

Short answer: a Bash for loop runs the same commands once for each item in a list: for item in one two three; do echo "$item"; done. Use {1..10} for a fixed range of numbers, for (( i = 1; i <= n; i++ )) when you need a counter, and for f in *.log to work through files. Every example below was run in Bash 5.2 on Debian 13, and the output shown is what we got.

Terminal showing Bash for loops over a word list, a number range, a C-style counter and files on a Debian 13 server
Bash for loops run on a Debian 13 server: a word list, {1..5}, a C-style counter and a loop over files.

The basic syntax

The list form is the one you will use most. Bash expands the words after in, sets the variable to each word in turn, and runs everything between do and done:

for name in alpha beta gamma; do
  echo "Hello, $name"
done
Hello, alpha
Hello, beta
Hello, gamma

You can write the same loop on one line. The semicolons stand in for the line breaks:

for i in 1 2 3; do echo "Line $i"; done

Two details from the Bash manual are worth knowing. If you leave out in ... completely, the loop runs over the script’s arguments ("$@"). And if the list expands to nothing, the body never runs and the loop returns status 0.

Loop over a range of numbers

Brace expansion builds the list for you. You can add a step, pad with zeros or count through letters:

for i in {1..5}; do echo "Number $i"; done      # 1 to 5
for i in {0..20..5}; do echo -n "$i "; done     # 0 5 10 15 20
for i in {01..10}; do echo -n "$i "; done       # 01 02 ... 10
for c in {a..e}; do echo -n "$c "; done         # a b c d e

There is one trap here. Brace expansion happens before variable expansion, so a variable inside the braces does not work:

n=5
for i in {1..$n}; do echo "$i"; done
{1..5}

The loop ran once with the literal text {1..5}. When the end of the range comes from a variable, use seq or the C-style loop below:

for i in $(seq 1 "$n"); do echo -n "$i "; done   # 1 2 3 4 5

The C-style for loop

Bash also has an arithmetic form with a start, a test and a step, like C or JavaScript. It reads variables directly, so it is the cleanest choice for counters:

n=5
for (( i = 1; i <= n; i++ )); do
  echo -n "$i "
done
# 1 2 3 4 5

for (( i = 10; i > 0; i -= 3 )); do echo -n "$i "; done
# 10 7 4 1

Loop over files

Put a wildcard pattern in the list and Bash replaces it with the matching file names, sorted. Always quote the variable, because file names can contain spaces:

for f in *.log; do
  echo "Found: $f"
done
Found: access.log
Found: error.log
Found: my site.log

Do not loop over the output of ls. In the same folder, for f in $(ls *.log) printed my and site.log as two separate files, because the output is split on spaces.

Also watch what happens when nothing matches. Without extra options, the pattern stays as plain text, so this loop runs once with a file name that does not exist:

for f in *.gz; do echo "Found: $f"; done
# Found: *.gz

Turn on nullglob and a pattern with no matches expands to nothing, so the loop simply does not run:

shopt -s nullglob
for f in *.gz; do echo "Found: $f"; done
# (no output)

Loop over an array

Use "${array[@]}" with the quotes, so an item with a space stays one item. To get the index as well, loop over "${!array[@]}":

servers=("web1" "web2" "db 1")

for s in "${servers[@]}"; do
  echo "Server: $s"
done

for i in "${!servers[@]}"; do
  echo "$i: ${servers[$i]}"
done
Server: web1
Server: web2
Server: db 1
0: web1
1: web2
2: db 1

Reading a file line by line: use while, not for

A common mistake is for line in $(cat file). It splits on every space, not on line breaks. With a file containing example.com, example.org and my site.test, it gave four items instead of three. A while read loop keeps each line whole:

while IFS= read -r domain; do
  echo "[$domain]"
done < domains.txt
[example.com]
[example.org]
[my site.test]

Skip or stop: continue and break

continue jumps to the next item and break leaves the loop. This prints odd numbers and stops after 7:

for i in {1..10}; do
  if (( i % 2 == 0 )); then continue; fi
  if (( i > 7 )); then break; fi
  echo -n "$i "
done
# 1 3 5 7

For more on the if tests used here, see Understanding Bash if…else statements.

Practical examples for servers

Check the HTTP status of several URLs

for url in https://example.com/ https://example.com/missing-page; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "$url")
  echo "$code  $url"
done
200  https://example.com/
404  https://example.com/missing-page

Look up the IP address of several domains

for d in example.com example.org; do
  echo "$d: $(dig +short A "$d" | head -n1)"
done

This prints each domain with its first IPv4 address. See DNS lookup commands for more dig options.

Compress log files and rename extensions

for f in *.log; do gzip -- "$f"; done

for f in *.jpeg; do mv -- "$f" "${f%.jpeg}.jpg"; done

The -- stops a file name that starts with a dash from being read as an option. ${f%.jpeg} removes .jpeg from the end of the name. In our test, c d.jpeg became c d.jpg without trouble because the variables are quoted.

Run jobs in parallel and wait for all of them

for i in {1..3}; do
  sleep 1 &
done
wait
echo "all done"

The & starts each command in the background and wait pauses until they have all finished. Keep the number of parallel jobs small on a busy server.

Quick reference

You want to loop overUseWatch out for
A few fixed wordsfor x in a b cQuote "$x" when you use it
A fixed rangefor i in {1..10}No variables inside the braces
A range from a variablefor (( i = 1; i <= n; i++ ))Or $(seq 1 "$n")
Filesfor f in *.logNever $(ls); consider shopt -s nullglob
An arrayfor s in "${arr[@]}"Keep the quotes and the [@]
Lines of a filewhile IFS= read -r linefor splits on spaces
Script argumentsfor arg (no in)Same as for arg in "$@"

Before you run a loop on a live server

  • Do a dry run first: put echo in front of the real command (echo rm -- "$f") and read what it would do.
  • Quote every variable: "$f", not $f.
  • Use full paths in scripts that run from cron, because the working directory may not be what you expect. See how to list cron jobs and read crontab syntax.
  • Test on a copy of the data, not the only copy.

New to shell scripts? Start with creating a Hello World script, then keep the Linux commands cheat sheet nearby. If you want a place to practise with full root access, a KVM VPS gives you your own Linux server to script against.

Ithal P R
Ithal P R

Hi, I'm Ithal! I write tech and hosting blogs at Ucartz.com, where I share insights, tips, and the latest updates from the world of web hosting and technology. Whether it's about server setups, control panels, or tools to power your online presence, I break things down in a way that's easy to understand, even if you're just starting out.