Automating repetitive work with the `for` loop: from the word-list form, the `{1..10}` range with `{1..10..2}` steps, to the C-style `for ((i=0; i<10; i++))`. Complete with `break`, `continue`, iterating files via glob, and mass rename practice along with the traps that commonly catch people.

In episode 10 we covered advanced conditionals — case ... esac for matching many patterns at once and the =~ operator for regex validation. In this episode we open a topic just as fundamental: loops. If conditionals let your scripts think, loops let your scripts work.
Imagine you're assigned to change the extension of 500 files from .txt to .md one by one. Manually, that's hours of boring, error-prone work. With a loop, that's a ten-line script done in a fraction of a second. This is the difference between humans and machines: machines never get bored repeating the same thing. Loops are how you command the machine to exploit that advantage.
In this episode we focus on for — the most commonly used loop in Bash. We'll dissect its three forms: the word-list form, the numeric range form, and the C-style form. We'll close with break, continue, and a real-world mass rename practice. The next episode will follow with while, until, and reading files line by line.
Before dissecting syntax, it's important to understand why loops are the main reason people write scripts in the first place. Loops turn linear work into exponential work: one command you write once can affect dozens, hundreds, even thousands of items.
Compare these two approaches. To delete stale backup files:
rm backup-2024-01-01.tar.gz
rm backup-2024-01-02.tar.gz
rm backup-2024-01-03.tar.gz
for f in backup-*.tar.gz; do
rm "$f"
doneThe diff lines above capture the essence of automation: instead of writing one command per file, you write one command for all files. When a 31st file appears tomorrow, the first approach needs new code; the second approach works unchanged. Loops are a data-driven way of thinking: the script no longer cares how many items there are — what matters is the pattern.
for item in a b c (Word List)The most basic form of for iterates over a list of space-separated words:
for kota in Jakarta Bandung Surabaya; do
echo "Kota: $kota"
doneReading this line: "for every kota in the list Jakarta Bandung Surabaya, run the do...done block." On the first iteration kota holds Jakarta, then Bandung, then Surabaya, and the loop finishes. Think of it as a factory conveyor belt: the word list is the boxes passing by, and the do...done block is a workstation that processes one box each time.
The kota variable is the loop variable — it lives for the duration of the loop and changes every iteration. You're free to name it anything: file, i, user, server, and so on. What matters is consistency inside the block.
The word list can also come from variable expansion:
server_list="web-01 web-02 db-01"
for server in $server_list; do
echo "Menyiapkan deploy ke $server"
done{1..10} and {1..10..2}Writing for i in 1 2 3 4 5 6 7 8 9 10 is boring and typo-prone. Bash provides brace expansion to generate number sequences automatically:
for i in {1..10}; do
echo "Iterasi ke-$i"
done
for genap in {0..10..2}; do
echo "Angka genap: $genap"
done{1..10} produces 1 2 3 4 5 6 7 8 9 10, and {0..10..2} produces 0 2 4 6 8 10 — the start number, the end number, and the step. The {start..end..step} syntax is modern Bash; make sure your Bash is version 4 or later for full support.
Brace expansion isn't only for numbers. You can generate letter sequences, or more creative combinations:
for huruf in {a..f}; do
echo "Huruf: $huruf"
doneTip
Brace expansion happens before the loop runs, not during iteration. That means {1..1000000} on the right-hand side will build a one-million-word list in memory first. For very large loops, the C-style form (discussed shortly) is more efficient because it doesn't need a materialized list upfront.
for ((i=0; i<10; i++))The third form mimics C language syntax and is very useful when you need precise numeric control — indices, counters, or sequences with a dynamic stop condition:
for ((i=0; i<10; i++)); do
echo "Hitungan ke-$i"
doneThe for (( ... )) structure has three clauses separated by ;:
| Clause | Example | Function |
|---|---|---|
| Initialization | i=0 | Runs once at the start, sets up the initial value |
| Condition | i<10 | Evaluated before each iteration; the loop stops when false |
| Increment | i++ | Runs after each iteration, updates the counter |
Notice that the i variable here doesn't use $ in the initialization and increment clauses — writing $i=0 is actually wrong. This is one habit you need to carry over from other programming languages: inside (( )), variable names are written without $.
The C-style form is the right choice when the iteration count is computed — for example, trying a connection 5 times before giving up:
for ((attempt=1; attempt<=5; attempt++)); do
echo "Percobaan ke-$attempt..."
if ping -c 1 server-db >/dev/null 2>&1; then
echo "Koneksi berhasil!"
break
fi
sleep 2
donebreak and continue: Controlling Loop FlowSometimes you don't want to run every iteration. These two keywords give you control like the brake and accelerator on a loop:
break — stops the loop immediately, jumping out of do...done without waiting for the iteration to finish.continue — skips only the current iteration and jumps straight to the next item.An example using continue to skip files that aren't targets, and break to stop entirely:
for file in *.log; do
if [[ "$file" != app-* ]]; then
continue
fi
echo "Memproses $file"
done
for i in {1..10}; do
if (( i > 5 )); then
break
fi
echo "Masih di angka $i"
doneNote
Use continue for filtering and break for early exit. Reading code full of nested if...else inside a loop gets tiring fast; continue moves the exclusion logic to the top so the main flow stays readable. Remember this pattern: check, then continue, then do the work — very effective in long scripts.
One of the most common real-world uses of for is iterating over files in a directory. The key: let glob expansion produce the file list, then wrap the variable in quotes when using it:
for foto in *.jpg; do
echo "Memproses $foto"
# misal: resize, konversi, atau rename
doneThe for foto in *.jpg form makes Bash expand *.jpg into the list of matching files — and importantly, this list is already separate items, not one long string. This differs from for foto in $(ls *.jpg), which is prone to problems with filenames containing spaces.
Also note the edge case: if there are no .jpg files, a glob without nullglob still produces the literal string *.jpg and the loop runs one iteration with $foto = *.jpg. To prevent this, enable the nullglob option:
shopt -s nullglob
for foto in *.jpg; do
echo "Memproses $foto"
doneLet's tie it all together in a real case study that admins need all the time: mass rename. Suppose you have a set of log files with the old .log extension that must be changed to .txt:
#!/usr/bin/env bash
shopt -s nullglob
for file in *.log; do
baru="${file%.log}.txt"
mv "$file" "$baru"
echo "Renamed: $file -> $baru"
doneLet's dissect the key line: "${file%.log}.txt" uses parameter expansion to remove the .log suffix from the filename (% cuts from the back), then inserts .txt. The mv "$file" "$baru" technique with both arguments quoted is the golden rule for all file operations: always quote variables holding filenames, because filenames on Linux may contain spaces, parentheses, even newlines.
Caution
Always test a mass rename script on a copy of the data first. One typo in the name-stripping pattern can result in dozens of files overwritten or lost without a trace. A healthy habit: run a dry-run version (for example, only echo "$file -> $baru") before the real version containing mv. In production, mv can also be strengthened with the -n option (no-clobber) so it won't overwrite existing files.
for Loops| Mistake | Symptom | Solution |
|---|---|---|
for file in $list without quoting on use | A spaced filename splits into two items | Always "$file" when using the variable |
for i in $(command) instead of array/glob | Output splits on spaces & glob chars | Use an array (mapfile) or a literal glob |
Forgetting (( )) in C-style, writing for (i=0; ...) | Error syntax error near unexpected token '(' | Write for (( ... )) with double parens |
Increment clause i++ using $i++ | Value never increments correctly / warning | Write i++ without $ inside (( )) |
Glob without nullglob when no files exist | Loop runs once with literal *.jpg | shopt -s nullglob before the loop |
One mistake that most often confuses beginners is looping over command substitution output:
for i in $(ls *.txt); do
echo "$i"
doneThe $(ls *.txt) line runs ls, then its result is split on spaces (word splitting) and glob characters get expanded too. The result: a file named catatan penting.txt becomes two iterations (catatan and penting.txt), and a file named dokumen[1].txt can change meaning. The solution, as discussed, is a literal glob (for i in *.txt) or an array:
for i in *.txt; do
echo "$i"
done
mapfile -t files < <(find . -name "*.txt")
for i in "${files[@]}"; do
echo "$i"
doneWarning
The worst mistake of all: an infinite loop in the C-style for ((i=0; i<10; i++)) when the increment clause is written wrongly, for example i-1 instead of i+1 — the value of i never increases, so the condition i<10 stays true forever. Always check that the increment clause genuinely moves the counter toward the stop condition. As a safety net, run new loops with timeout 5 ./script.sh while writing them.
In this episode 11 we've mastered the for loop in its three forms: the word list (for item in a b c), brace expansion ranges ({1..10} and {1..10..2}), and the C-style (for ((i=0; i<10; i++))). We also learned break to stop a loop immediately, continue to skip one iteration, file iteration via glob with nullglob, and the mass rename practice that is one of the most common real-world uses. Just as importantly, we noted the classic traps: unquoted variables, word splitting on $(...), and empty globs.
The principle to take home: the glory of looping is the courage to write one command for thousands of items. No matter where the item list comes from — word list, number range, glob results, or command output — the key is always the same: let Bash split the list into items, and quote every variable when you use it.
But for isn't the only loop in Bash. There are situations where you don't know in advance how many times you need to repeat — for example, reading a file of unknown length, or waiting for a condition to be met. For that, Bash provides while and until. In the next episode, episode 12, we'll cover loops part 2: while, until, and reading files line by line — complete with CSV processing and the subshell traps that often make variables mysteriously "disappear". See you in the next episode!