Read file lines with `while read`, not `for`

`for line in $(cat file)` splits on whitespace, not lines. SC2013 recommends a `while IFS= read -r` loop instead.

Problem

`for line in $(cat file)` (or `$(<file)`) first performs command/parameter substitution, which is then word-split on `IFS` (spaces, tabs, and newlines by default) and globbed. The loop variable ends up bound to whitespace-separated words, not lines — a file with "hello world" on one line produces two iterations, not one. This also silently mangles leading/trailing whitespace, collapses blank lines, and expands any `*`/`?` characters present in the file content.

Bad

for line in $(cat file.txt); do
  echo "Line: $line"
done

Good

while IFS= read -r line; do
  echo "Line: $line"
done < file.txt

Explanation

`while IFS= read -r line` reads exactly one line per iteration, preserves leading/trailing whitespace by clearing `IFS`, and `-r` prevents backslash sequences from being interpreted. Redirecting the file with `< file.txt` avoids a useless `cat` and keeps the loop running in the current shell (unless piped, in which case it runs in a subshell).

Related

SC2086

SC2044

SC2162

When It Matters

for line in $(cat file) does not iterate lines: it iterates whitespace-separated words, then globs each one. A line containing two words produces two iterations, an empty line disappears, and a line containing an asterisk expands to the directory contents. Any script processing a list of paths, a CSV, or log lines will encounter all three cases, and the resulting off-by-many behaviour is not reported as an error.

Second Example

Note

Beware that commands reading stdin inside the loop — ssh is the classic — consume the input file. Pass ssh -n, or redirect the loop from a different file descriptor.

Exceptions

When the file genuinely contains whitespace-free tokens and you want word iteration — a list of package names or numeric IDs — the for loop is fine and more readable. Disable globbing with set -f if any token could contain a metacharacter.

Faq

Q

Why does my loop stop after one iteration?

A

A command inside the loop read the rest of stdin. Give ssh the -n flag, or read the file on a dedicated descriptor: while ... read -r line <&3; done 3< file.

Q

How do I keep variables set inside the loop?

A

Redirect from the file rather than piping into the loop; a pipeline puts the loop in a subshell and discards its assignments.

Q

How do I handle the last line without a newline?

A

Use while IFS= read -r line || [ -n "$line" ]; do to process the trailing partial line.