# for, while and Reading Lines Safely — Bash / Shell Scripting

Source: https://www.skillbyai.com/en/bash/c-loops

> Loop over values, files and lines without breaking on spaces.

## Iterating correctly

**`for item in list; do ...; done`** iterates over words: `for f in *.csv` loops over files matching a glob (each file name stays intact even with spaces, because globbing produces separate words), and `for arg in "$@"` loops over arguments. **C-style** loops count: `for (( i = 0; i < 5; i++ ))`. **`while condition; do ...; done`** repeats while a command succeeds, and **`until`** while it fails, useful for retries. To process a file or command output **line by line**, use `while IFS= read -r line; do ...; done < file`: `IFS=` keeps leading and trailing spaces, and `-r` keeps backslashes literal. Do **not** use `for line in $(cat file)`, which splits on every space and expands globs. When piping into `while`, note that the loop runs in a **subshell** in Bash, so variables set inside are lost afterwards; feed the loop with `< file` or `< <(command)` instead. `break` and `continue` control loop flow; enable `shopt -s nullglob` so a glob with no matches expands to nothing instead of the literal pattern.

## Safe loops over files and lines

Globs and `read -r` handle awkward names and lines correctly.

```bash
shopt -s nullglob                    # no matches -> empty list, not literal "*.csv"
for f in /data/incoming/*.csv; do
    echo "processing: $f"
done

total=0
while IFS= read -r line; do
    [[ -z $line || $line == \#* ]] && continue   # skip blanks and comments
    total=$(( total + 1 ))
done < hosts.txt
echo "hosts: $total"                # works: the loop did not run in a subshell

# reading command output without losing variables
while IFS=, read -r user email; do
    echo "$user <$email>"
done < <(grep -v '^#' users.csv)

attempt=0
until curl -fsS https://example.com/health > /dev/null; do
    (( ++attempt >= 5 )) && { echo "giving up" >&2; exit 1; }
    sleep $(( attempt * 2 ))
done
```

## cat | while loses your variables

`cat file | while read -r line; do count=$((count+1)); done` leaves `count` unchanged afterwards, because the loop ran in a subshell. Redirect the file into the loop instead.

**Quiz:** Why is `while IFS= read -r line` preferred for reading lines?

- [ ] It is faster than every other method
- [ ] It reads the whole file into memory at once
- [x] It preserves spaces and backslashes exactly as they appear in each line
- [ ] It sorts the lines

*Answer:* It preserves spaces and backslashes exactly as they appear in each line. Empty IFS keeps surrounding whitespace and -r stops backslash interpretation.
