Learn BASH Scripting - Loops Part 2: while, until & Reading Files
Episode 12 of 27

Learn BASH Scripting - Loops Part 2: while, until & Reading Files

Continuing loops with `while`, which runs while a condition is true, `until`, which runs until a condition is met, and the `while true` pattern for daemons. Also covers reading files line by line with `while read -r line`, CSV processing, and the subshell trap that makes variables "disappear" after a pipeline.

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

Introduction

In episode 11 we mastered the for loop — from word lists, {1..10} ranges, to the C-style for ((i=0; i<10; i++)). In this episode we complete the loop arsenal with its partners: while and until.

for is like a factory sorter who knows exactly how many boxes will pass: it needs a complete list before starting. But many jobs don't know how many times they need to repeat before finishing — waiting for a service to come up, reading a file whose lines can grow, or waiting for a user to type the right answer. For these jobs, while and until are the right tools: they repeat while a condition holds, not while items remain.

This episode's biggest bonus: reading files line by line with while read. Almost every operational script — log parsers, configuration file processing, CSV handling — rests on this pattern. We'll also dissect one of Bash's most famous mysteries: why variables inside while often "disappear" after the pipeline finishes (the answer: subshell). Let's get started.

Main Discussion

while: Repeat While the Condition Is True

while is the most flexible loop. It evaluates a condition — any command or test — and as long as that condition returns exit status 0 (success), the do...done block keeps running:

Basic while structure
count=0
while [ "$count" -lt 5 ]; do
    echo "Hitungan: $count"
    count=$((count + 1))
done
As long as count < 5, the block keeps running

Reading the code above: "as long as $count is less than 5, print its value then add 1." Without the count=$((count + 1)) line, the condition would never change and you'd be stuck in an infinite loop. This is a classic mistake — the condition and the change to that condition are two things that must always coexist in a condition-based loop.

Think of while as a camp watchtower furnace: the fire is kept burning as long as there's wood ([ -n "$kayu" ]). Every iteration you check the wood, and when it runs out, the fire goes out. Bash itself evaluates the condition at the start of every iteration — before the block executes.

until: Repeat Until the Condition Is True

until is the logical opposite of while — this isn't just cosmetic, it's a difference in how you think:

  • while [ kondisi ] → run while the condition is true.
  • until [ kondisi ] → run until the condition is true (stop as soon as it becomes true).
until runs while the condition is false
count=0
until [ "$count" -ge 5 ]; do
    echo "Masih di angka $count"
    count=$((count + 1))
done
Stops when count reaches 5

In terms of results, the example above is identical to the while version. The difference is in readability: choose until when the stop condition is more natural to say than the continue condition. Imagine waiting for a user to answer "yes":

Waiting for confirmation with until
jawaban=""
until [ "$jawaban" = "ya" ]; do
    read -p "Konfirmasi (ketik 'ya'): " jawaban
done
echo "Dikonfirmasi!"
The loop stops as soon as the answer is yes, no matter how many times asked

The mental reading: "keep asking until he answers yes." Far more readable than while [ "$jawaban" != "ya" ], which forces you to think in negation.

The while true + break Pattern: Lightweight Daemon

There's one pattern hugely popular in daemon and watcher scripts: while true — a loop that never stops on its own, stopped explicitly via break from inside. This pattern fits when the stop condition is hard to write at the top of the loop (for example, when it depends on the result of the iteration's work):

The while true pattern with break
while true; do
    if [ -f /tmp/stop-dengan-fresh ]; then
        echo "File penanda terdeteksi, berhenti."
        break
    fi
    echo "Menjalankan tugas rutin... (Ctrl+C untuk berhenti)"
    sleep 10
done
A simple daemon that stops when a marker file appears

The true command is a Bash builtin that always returns exit status 0 — a condition that never becomes false, so the loop never stops on its own condition. Inside, break becomes the only deliberate way out. This pattern also often hosts continue to skip one cycle:

while true with continue and break
while true; do
    read -p "Perintah: " cmd
    case "$cmd" in
        next)   continue ;;
        quit)   break ;;
        *)      echo "Eksekusi: $cmd" ;;
    esac
done
echo "Selesai."
continue skips a cycle, break ends everything

Note

The combination of case (episode 10) + while true + break forms the core of almost every interactive REPL and menu loop in Bash scripts. This pattern is also the basis of watchdog scripts that monitor a process and restart it when it dies. Memorize the pattern: while true for an eternal cycle, case to translate input, continue/break for exit control.

Reading Files Line by Line

Now we get to the most important pattern in this episode. To process a file line by line, the standard formula is:

Reading a file line by line
while read -r line; do
    echo "Baris: $line"
done < file.txt
while read -r line; do ... done < file.txt

Let's dissect each part:

  1. while read -r line — each iteration, the read command takes one line from stdin and stores it in the line variable, then returns exit status 0 (successfully read). When the file runs out, read returns a non-zero status and the loop stops. The beauty of this pattern: the loop knows when to stop on its own, without counting lines.
  2. done < file.txt — the input redirection attaches to done, not the start. This tells: "feed every line of file.txt as stdin for the read command inside the loop."

In fact, read is the key here — each iteration it consumes one line and moves the cursor to the next line. It's like a conveyor belt where you take one document each time until the stack runs out.

Why read -r?

The -r option on read may seem trivial, but it saves you from a very subtle bug. Without -r, read treats backslash (\) as an escape character — a character that modifies the next character:

The effect of -r on backslashes
echo 'C:\data\server\logs.txt' > path.txt
 
read path < path.txt
echo "Tanpa -r : $path"
# Tanpa -r : C:dataserverlogs.txt   <- backslash hilang!
 
read -r path < path.txt
echo "Dengan -r: $path"
# Dengan -r: C:\data\server\logs.txt
Without -r, backslashes in the file are digested as escapes

Why does read without -r remove backslashes? Because read is designed to support line continuation — a backslash at the end of a line means "continue on the next line". Without -r, even backslashes in the middle of a line get digested. For real-world data — Windows paths, regex, escape sequences — backslash is valid data that must be preserved. The golden rule: always write read -r unless you have an explicit reason not to.

IFS to Preserve Leading Spaces

There's a second, subtler trap: read by default strips spaces and tabs at the beginning and end of a line. That's because the default IFS (Internal Field Separator) contains space, tab, and newline. For files whose lines intentionally have leading spaces (for example, source code or indented output), use IFS= before read:

Empty IFS preserves leading spaces
while IFS= read -r line; do
    echo ">>> $line <<<"
done < indentasi.txt
IFS= keeps the line's leading spaces intact

Note the writing: while IFS= read -r line — the IFS= assignment is directly on the same line as read. This is a one-command variable assignment (command assignment), which only applies to that single command. With an empty IFS, read doesn't trim any space characters from the line.

Important

Always pair read with -r, and use IFS= when file lines contain leading spaces that must be preserved. The complete safe formula for almost any file: while IFS= read -r line; do ...; done < file. The three elements — IFS=, read -r, and done < file — are a package that can't be assembled piecemeal: removing any one of them means deferring a weird bug that only appears on certain data.

Reading CSV: while IFS=',' read -r f1 f2

read's most commonly used strength in the real world is its ability to split one line into several fields at once. Just give read a list of variables — it divides the line based on IFS and fills each variable:

Reading CSV with a comma separator
while IFS=',' read -r nama umur kota; do
    echo "Nama : $nama"
    echo "Umur : $umur"
    echo "Kota : $kota"
    echo "---"
done < data.csv
The line 'arman,30,jakarta' splits into three variables

For each line like arman,30,jakarta, read splits it on commas and fills $nama=arman, $umur=30, $kota=jakarta. If a line has fewer fields than variables, the rest stay empty; if more, the last field holds the remainder.

A closer-to-DevOps example: the servers.csv file contains a list of servers and their roles, then the script pings them all:

Mass ping from CSV
while IFS=',' read -r host role; do
    if ping -c 1 -W 2 "$host" >/dev/null 2>&1; then
        echo "OK   : $host ($role)"
    else
        echo "FAIL : $host ($role)"
    fi
done < servers.csv
Each CSV line becomes one ping target

Tip

For CSV with empty lines or comments (lines starting with #), add a filter at the top of the block: [[ -z "$host" || "$host" == \#* ]] && continue. This "check, then continue" pattern ensures the loop doesn't process junk data. And remember: read splits on single characters in IFS — for multi-character delimiters (e.g. ||), use another tool like awk -F'\|\|'.

The Subshell Mystery: Why Variables "Disappear" After a Pipeline

This is one of Bash's most famous mysteries and one that most often makes admins bang their heads against the wall. Try running this script:

The variable lost after a pipeline
total=0
cat data.txt | while read -r baris; do
    total=$((total + 1))
done
echo "Total baris: $total"   # output: Total baris: 0  <- BUG!
total stays 0 — changes in the pipeline aren't visible outside

You expect total to hold the line count, but the output is 0. Why? Because every element in a pipeline (cat ... | while ...) runs in a subshell — a child Bash process with its own copy of the variables. The assignment total=$((total + 1)) happens in the subshell, and when the subshell finishes, all its changes are discarded. $total in the parent shell is never touched.

The analogy: you send a courier (subshell) carrying a notebook (variable copy). The courier writes down numbers, but the notebook he carries is a photocopy — once he leaves, the original notebook stays blank.

There are several solutions. The simplest: don't use a pipeline at all — use direct redirection:

Solution 1: redirection, not a pipeline
total=0
while read -r baris; do
    total=$((total + 1))
done < data.txt
echo "Total baris: $total"
The loop runs in the same shell, variables are saved

An alternative solution for certain cases — if you truly need a pipeline (for example, because another process filters the data), use process substitution or move the result into a variable with mapfile:

Solution 2: process substitution & mapfile
total=0
while read -r baris; do
    total=$((total + 1))
done < <(grep -v '^#' data.txt)
echo "Total: $total"
 
mapfile -t baris_baris < data.txt
echo "Jumlah baris: ${#baris_baris[@]}"
Both approaches preserve variable changes

Warning

The principle to remember: every command in a a | b | c pipeline runs in a subshell, and all variables changed inside it won't be visible after the pipeline finishes. This applies not only to while — it also applies to other loops connected to a pipeline. If a variable "won't change" in your script, suspect the pipeline as the cause first. Rule of thumb: use done < file (redirection) or while ... done < <(command) (process substitution), not a pipeline.

Common Mistakes in while, until, and Reading Files

MistakeSymptomSolution
Forgetting to update the condition inside the loopInfinite loopMake sure a command changes the condition each iteration
`while ... donecommand` (pipeline)Variables lost after the loop
read line without -rBackslashes in data lost/digestedAlways read -r line
Forgetting done < fileThe loop doesn't read the file, read waits on stdinAttach the redirection to done
while read with lines having leading spacesLeading spaces trimmedwhile IFS= read -r line
Typing while without ; do or a separate dosyntax error near unexpected tokenFormat: while [ kondisi ]; do ... done

Caution

The infinite loop is the most dangerous enemy of this episode. In the terminal, Ctrl+C stops it — but in non-interactive scripts (cron, CI), that button doesn't exist. A lifesaving habit: always bound loops involving external conditions, for example a maximum attempt counter: for ((attempt=0; attempt<10; attempt++)); do [ kondisi ] && break; sleep 5; done. And when testing new scripts, run them with timeout 10 ./script.sh as a safety net.

Conclusion

In this episode 12 we completed Bash's loops with while (runs while a condition is true), until (runs until a condition is met), and the while true + break pattern for daemons and REPLs. More importantly, we mastered the most productive pattern in Bash scripting: while IFS= read -r line; do ...; done < file for reading files line by line, splitting CSV into fields with IFS=',', and revealed the subshell mystery — why variables changed in a pipeline "disappear" and how to avoid it.

The principle to take home: for for known lists, while/until for evaluated conditions, and always read -r (plus IFS= when needed) when touching data from files. With the combination of conditionals and loops, your scripts can now read, evaluate, and repeat — three core abilities of automation.

But there's one issue starting to be felt: your code is getting longer, and the same patterns (for example, "read file, filter, report") are starting to repeat in many places. In the next episode, episode 13, we'll discuss the solution: functions — how to wrap logic into reusable blocks, complete with local variables, arguments, and return values. That's where your scripts change from merely executing tasks to clean code architecture. See you in the next episode!

Learn BASH Scripting - Loops Part 2: while, until & Reading Files | Learn BASH Scripting