Learn BASH Scripting - Loops Part 1: for
Episode 11 of 27

Learn BASH Scripting - Loops Part 1: for

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.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

In episode 10 we covered advanced conditionalscase ... 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.

Main Discussion

Why Are Loops the Core Power of Scripting?

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:

Without a loop vs with a loop
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"
done

The 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.

First Form: for item in a b c (Word List)

The most basic form of for iterates over a list of space-separated words:

Simple for over a word list
for kota in Jakarta Bandung Surabaya; do
    echo "Kota: $kota"
done
The item variable changes value in each iteration

Reading 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:

For over a list from a variable
server_list="web-01 web-02 db-01"
for server in $server_list; do
    echo "Menyiapkan deploy ke $server"
done
Each variable element becomes one iteration

Second Form: Range {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:

Number range with brace expansion
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} and {1..10..2} generate the sequences

{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:

Letter ranges and combination patterns
for huruf in {a..f}; do
    echo "Huruf: $huruf"
done
{a..z} generates a letter sequence

Tip

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.

Third Form: C-Style 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:

C-style for loop
for ((i=0; i<10; i++)); do
    echo "Hitungan ke-$i"
done
Three parts: initialization, condition, increment

The for (( ... )) structure has three clauses separated by ;:

ClauseExampleFunction
Initializationi=0Runs once at the start, sets up the initial value
Conditioni<10Evaluated before each iteration; the loop stops when false
Incrementi++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:

Retry with a C-style loop
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
done
Try up to 5 times then give up

break and continue: Controlling Loop Flow

Sometimes 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:

continue and break in action
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"
done
continue skips one iteration, break stops the entire loop

Note

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.

Iterating Files with Glob

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:

Iterating all .jpg files
for foto in *.jpg; do
    echo "Memproses $foto"
    # misal: resize, konversi, atau rename
done
Glob generates the file list; make sure the variable is quoted

The 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:

Avoiding empty glob iterations
shopt -s nullglob
for foto in *.jpg; do
    echo "Memproses $foto"
done
nullglob makes an empty glob produce an empty list

Practice: Mass File Rename

Let'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:

Mass rename of log files
#!/usr/bin/env bash
shopt -s nullglob
 
for file in *.log; do
    baru="${file%.log}.txt"
    mv "$file" "$baru"
    echo "Renamed: $file -> $baru"
done
Quoting variables prevents filenames with spaces from splitting

Let'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.

Common Mistakes in for Loops

MistakeSymptomSolution
for file in $list without quoting on useA spaced filename splits into two itemsAlways "$file" when using the variable
for i in $(command) instead of array/globOutput splits on spaces & glob charsUse 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 / warningWrite i++ without $ inside (( ))
Glob without nullglob when no files existLoop runs once with literal *.jpgshopt -s nullglob before the loop

One mistake that most often confuses beginners is looping over command substitution output:

The wrong pattern: word splitting in $(...)
for i in $(ls *.txt); do
    echo "$i"
done
Output with spaces or glob characters splits uncontrollably

The $(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:

The right pattern: glob or array
for i in *.txt; do
    echo "$i"
done
 
mapfile -t files < <(find . -name "*.txt")
for i in "${files[@]}"; do
    echo "$i"
done
A direct glob, or mapfile for multi-line output

Warning

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.

Conclusion

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!

Learn BASH Scripting - Loops Part 1: for | Learn BASH Scripting