Loops: for, while & reading files
Repeat work and read input line by line.
Doing the same job on a pile of things
You have forty servers to check, two hundred log files to scan, or a list of user accounts to audit. Typing the same command forty times is slow and easy to fumble. A loop is the fix. It is the assembly-line worker of Bash (the command language you type into a Linux terminal): you describe the steps once, hand it a pile of items, and it runs those steps on each item in turn. This lesson covers the loops you will actually reach for in operations work, plus the one file-reading pattern that quietly breaks security scripts when people get it wrong.
for: one run per item in a list
A `for` loop is you reading names off a guest list. For each name you do the same thing: check them in. You write the list of items after the word `in`, and Bash runs the body once with the loop variable set to each item.
Bash split that list on spaces and ran the body three times. `$env` held `dev`, then `staging`, then `prod`. The words after `in` are the whole list, and they can be anything: hostnames, ports, filenames, usernames.
for over a glob: let the shell find the files
Most of the time you do not want to type the filenames by hand. You want to hand the shell a rule instead of a list: "every file whose name ends in `.log`". That shorthand is a glob (short for global, the old name for filename wildcards). The `*` matches any run of characters, so `*.log` means every name ending in `.log`. The shell expands the glob into the real filenames before the loop starts.
The shell replaced `*.log` with the three matching files (in alphabetical order) and the loop ran once per file. Notice the double quotes around `"$logfile"`. A filename can contain spaces, and the quotes keep such a name as one item instead of two. Leave them off and a file called `access log.txt` would split and break your script. One sharp edge: if no file matches, plain Bash leaves the text `*.log` unexpanded and the loop runs once with that literal string. Running `shopt -s nullglob` first makes a no-match glob expand to nothing, so the loop runs zero times, which is usually what you want.
Counting and waiting: C-style for and while
Two loops cover the cases where you are not walking a list. When you want to run something a fixed number of times, use the C-style `for`, named after the C programming language it copies. It works like a lap counter at a track: it starts at a number, keeps going while a condition holds, and ticks the number up each time around. Inside the double parentheses those three parts sit in order: where the counter starts, how long to keep going, and how the counter changes each pass.
When you do not know the count ahead of time and instead want to keep going until some condition flips, use `while`. It works like a bucket under a tap: keep filling while the bucket is not full. Bash checks the condition, runs the body, then checks again, stopping the moment the condition is false.
`(( count-- ))` subtracts one from `count` each pass. Forget that line and the condition never turns false, so you get an infinite loop. Press Ctrl-C to kill one if it happens.
Reading a file line by line, the right way
This is the pattern people get wrong most often, and the wrong version fails in ways that matter for security. Say you have an allowlist of addresses, where the exact content on each line matters:
10.0.0.510.0.0.6C:\logs\path*
Here is the correct way to read it. Memorize this line as a single unit:
Every line arrived exactly as written: the leading spaces on the second address, the backslashes in the path, and the lone `*`. Read the machinery from right to left. `< allowlist.txt` feeds the file into the loop as its input. `read` pulls one line into the variable `line` and returns success until the file runs out, which is what stops the loop at the end. Two small pieces carry the weight: `IFS=` and `-r`.
`IFS` is the Internal Field Separator, the set of characters Bash treats as gaps between words. Normally `read` uses it to trim leading and trailing spaces and tabs off each line. Setting `IFS=` (empty) for this one command turns that trimming off, so ` 10.0.0.6` keeps its indentation. `-r` means raw. Without it, `read` treats a backslash as an escape character and swallows it. The difference is not cosmetic:
Without `-r`, `C:\logs\audit` came back as `C:logsaudit`. The backslashes vanished. On Windows paths, Active Directory names, or regular expression (regex) patterns stored in a rules file, that silent damage changes what your script matches. Keep `-r` on every read loop.
Now the wrong way, the one you will find in old blog posts and copy-pasted scripts:
break and continue: skip one, or stop early
Inside any loop, two commands change the flow. `continue` says "skip the rest of this pass and move to the next item", like passing on a name on your checklist. `break` says "stop the whole loop now and walk away". A common shape reads a host list, skips comment lines, and halts at a marker:
web-01# web-02 is decommissionedweb-03STOPweb-04
The comment line was skipped by `continue`, `web-03` was scanned, and `STOP` ended the loop with `break` before `web-04` was ever read. `[[ "$host" == \#* ]]` is a pattern test: it is true when `host` starts with a `#`. The backslash before `#` keeps the shell from reading it as the start of a comment.
Putting it together: scan a folder of logs
Real work stacks these loops. Here is a script that walks every log file in a folder and, for each one, reads it line by line to count failed logins. The outer loop is a glob over files, and the inner loop is a `while read` over the lines of each file.
#!/usr/bin/env bashfor logfile in logs/*.log; dofails=0while IFS= read -r line; do[[ "$line" == FAIL* ]] && (( fails++ ))done < "$logfile"echo "$logfile: $fails failed login(s)"done
The `fails` counter is reset to zero at the top of each file, counts lines starting with `FAIL`, and gets printed once per file. Because the file is fed in with `< "$logfile"` rather than piped, the whole loop runs in your current shell, so the counter survives to the `echo` after it.