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?
answer
- three tokens, three different bugs
- default IFS trims the edges
- backslash is an escape by default
- one prefix assignment, one command
- EOF still returns non-zero
basics
~20 sIFS= 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 linesprintf ' 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.txtgo deeper
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.
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.
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.
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)