skip to content

The canonical bash idiom for reading a file line by line is `while IFS= read -r line; do ...; done < file.txt`. What does each of `IFS=`, `-r` and the redirection after `done` contribute, and what breaks if you leave them out?

level: middleimportance: must knowfreq 66%

answer

  1. three tokens, three different bugs
  2. default IFS trims the edges
  3. backslash is an escape by default
  4. one prefix assignment, one command
  5. EOF still returns non-zero

basics

~20 s

IFS= stops read stripping leading and trailing whitespace from the line, -r stops it treating backslashes as escapes, and the redirection after done feeds the whole loop from the file so it stays in the current shell.

solid answer

~50 s

`read` splits its input on the characters in `IFS` and, by default, strips leading and trailing ones — so with default `IFS` a line like ` indented` arrives as `indented`. Setting `IFS=` as a prefix on the `read` command empties it for that one command, so the line is preserved byte for byte. `-r` turns off backslash processing: without it, `read` treats a backslash as an escape, so a Windows path or a regex loses its backslashes and a line ending in a backslash silently swallows the next line. The `< file.txt` after `done` attaches the file to the whole loop, which avoids the subshell you would get from `cat file | while ...`. One caveat: `read` returns non-zero at end of file, so a final line with no trailing newline is read into `line` but the loop exits without processing it — guard with `while IFS= read -r line || [[ -n $line ]]`.

code

bash · 7 lines
bash
printf '  spaced  \nc:\\path\\file\n' > /tmp/demo.txt

echo '--- bare read ---'
while read line; do printf '[%s]\n' "$line"; done < /tmp/demo.txt

echo '--- IFS= read -r ---'
while IFS= read -r line; do printf '[%s]\n' "$line"; done < /tmp/demo.txt

go deeper

for a junior

Memorise the idiom exactly, including IFS=, -r and the redirect after done, and be able to say that it reads a file one whole line at a time without mangling spaces or backslashes.

for a middle

Explain each token's job separately: IFS= prevents trimming, -r prevents backslash escapes, the redirect avoids the extra process. Show the IFS=: variant that splits fields as it reads.

for a senior

Bring up the failure modes you have actually been burned by — a stripped indent that broke a generated config, a swallowed line ending in a backslash, a missing final line with no newline — and the guard for each.

for a principal

Argue for consistency: one blessed line-reading idiom in the team's script template, ShellCheck's SC2162 enforced in CI, and a clear rule for when data has outgrown line-at-a-time shell parsing and belongs in a real parser.

## The idiom ```bash while IFS= read -r line; do printf '[%s]\n' "$line" done < file.txt ``` Every token in that header is load-bearing. Interviewers ask about it because most people copy it without knowing which piece protects them from what, and each omission fails only on the inputs you did not test with. ## `read` and its defaults `read` is a shell builtin. It consumes one line from standard input, splits it into fields using the characters in `IFS`, assigns the fields to the named variables, and returns 0 — or non-zero when it hits end of file. With a single variable name, the *entire remaining line* is assigned to it, but the trimming still happens: leading and trailing `IFS` whitespace is removed. The default `IFS` is space, tab and newline, so: ```bash printf ' padded \n' | while read -r l; do printf '[%s]\n' "$l"; done # [padded] ``` For a config file that is often fine; for fixed-width data, for indented YAML, or for anything you will write back out unchanged, it is silent corruption. ## `IFS=` as a command prefix `IFS= read -r line` is an assignment *prefixed to a command*, which makes it apply to that command's execution only — `IFS` is back to its normal value on the next line of the script. With `IFS` empty there are no field separators, so nothing is split and nothing is trimmed; the line arrives exactly as it was in the file, minus its newline. The same mechanism gives you free field splitting when you *do* want it. Reading `/etc/passwd`: ```bash while IFS=: read -r user _ uid rest; do echo "$user has uid $uid" done < /etc/passwd ``` Setting `IFS=:` for that one `read` splits on colons; the final variable soaks up whatever is left over. ## `-r` Without `-r`, `read` treats backslash as an escape character: `a\tb` loses the backslash, and a line ending in `\` is joined to the next one. That mangles Windows paths, regexes, JSON and anything generated by a tool that escapes. There is essentially no situation where you want the default; ShellCheck warns `SC2162` ("read without -r will mangle backslashes") on every bare `read`. Treat `-r` as part of the command's spelling. ## The redirection after `done` `done < file.txt` attaches the file to the compound command, so every `read` in the loop draws from the same open file and consumes it progressively. The alternative you will see in the wild, `cat file.txt | while IFS= read -r line`, adds a pointless `cat` and — more importantly — makes the loop a pipeline stage, which runs it in a separate process and throws away anything the body assigns. If the input is a command rather than a file, keep the redirect shape and use process substitution: `done < <(some-command)`. ## The missing final newline `read` returns non-zero when it reaches EOF, *even if it assigned a partial line first*. A file whose last line has no trailing newline therefore loses that line: the value is in `line`, but the loop condition already failed. The standard guard is: ```bash while IFS= read -r line || [[ -n $line ]]; do ... done < file.txt ``` The `|| [[ -n $line ]]` lets one final iteration happen when `read` failed but still produced text. ## Why not `for` `for line in $(cat file.txt)` is the classic wrong alternative. It iterates over *words*, not lines: every space becomes a break, empty lines disappear, and any word containing a glob character is expanded against the filesystem. `while IFS= read -r` has none of those failure modes, which is why it is the answer interviewers are listening for. If you want the lines in an array rather than a loop, `mapfile -t lines < file.txt` (bash 4.0+) does the whole job in one builtin.

  • Why is `IFS=` written as a prefix rather than on its own line before the loop?
    A prefixed assignment applies only to that command's execution, so `IFS` is restored immediately afterwards. Setting `IFS=` on its own line changes it for the rest of the script, which silently alters word splitting everywhere else — a much bigger blast radius for the sake of one `read`.
  • How would you split each line into fields as you read it?
    Give `read` more variable names and set `IFS` to the delimiter for that command: `while IFS=: read -r user _ uid rest; do ...; done < /etc/passwd`. Bash assigns fields left to right and the last named variable receives all remaining fields, so put a catch-all at the end if there may be extra delimiters.
  • What happens to a file whose last line has no trailing newline?
    `read` assigns that partial line but returns non-zero because it hit EOF, so the loop condition fails and the body never runs for it. Guard with `while IFS= read -r line || [[ -n $line ]]`, which allows one last iteration when there is leftover text.

saying these in an interview costs you the question

  • Says -r means read raw bytes or read-only
  • Claims IFS= disables reading whitespace inside the line
  • Uses cat file | while read as the canonical form
  • Thinks read returns 0 forever and the loop needs a break
  • Iterates lines with for line in $(cat file)

context