skip to content

Bash scripts almost always read a file line by line as `while IFS= read -r line; do ...; done < input.txt`. Explain what the `IFS=` prefix and the `-r` flag each contribute, and what goes wrong if you drop them.

level: middleimportance: must knowfreq 70%

answer

  1. read is not a plain line reader
  2. it trims before it assigns
  3. backslash means something to read
  4. prefix assignment lasts one command
  5. IFS= keeps the indentation

basics

~20 s

IFS= empties the field separator for that one read, so the line is stored whole with leading and trailing whitespace intact. The -r flag stops read from treating backslashes as escapes. Without them, lines get trimmed and backslashes vanish.

solid answer

~40 s

`read` splits the line it just read using `IFS`, and it also strips leading and trailing IFS whitespace before assigning. Setting `IFS=` as a prefix on the command empties that separator set for that single invocation, so `line` gets the raw line including its indentation and trailing spaces — and, because the assignment is a command prefix, `IFS` is unchanged for the rest of the script. `-r` turns off backslash processing: without it, `read` treats a backslash as an escape character, so `C:\temp\new` comes back as `C:tempnew` and a line ending in a backslash is silently joined with the next one. Together they mean "give me this line exactly as it is in the file". The redirection `< input.txt` feeds the loop, and the loop ends when `read` returns non-zero at end of input.

code

bash · 9 lines
bash
printf '  indented  \nC:\\temp\\new\n' > /tmp/demo.txt

while IFS= read -r line; do printf '[%s]\n' "$line"; done < /tmp/demo.txt
# [  indented  ]
# [C:\temp\new]

while read line; do printf '[%s]\n' "$line"; done < /tmp/demo.txt
# [indented]
# [C:tempnew]

go deeper

for a junior

Memorise the idiom as a unit and use it whenever you read a file line by line: while IFS= read -r line; do ...; done < file. Know that dropping -r eats backslashes and dropping IFS= trims whitespace.

for a middle

Explain each token: read splits on IFS and trims IFS whitespace, so IFS= disables both for that call; -r stops backslash escape processing. Point out that the assignment is a command prefix, scoped to that one invocation.

for a senior

Demonstrate the operational edges — a final line without a trailing newline needing || [[ -n $line ]], and NUL-delimited input with -d '' when filenames may contain newlines — and be able to say why for line in $(cat f) is not an alternative.

for a principal

Frame it as data-format discipline: line-delimited text is a lossy interchange format for arbitrary content, so decide deliberately when a script should move to NUL-delimited records or to a real parser rather than hardening the read loop further.

## What `read` actually does `read` is a bash builtin that consumes one line from standard input — everything up to the next newline — and then does three things before you ever see it: 1. It strips leading and trailing characters that are **IFS whitespace**. 2. It splits the remainder into fields at **IFS** characters and assigns them to the named variables (with the last variable receiving all leftover fields). 3. Unless `-r` is given, it processes **backslashes** as escape characters, and a trailing backslash makes it continue onto the next line. Each of those is helpful at an interactive prompt and wrong when you are processing a file faithfully. `IFS= read -r line` switches off all three. ## Why `IFS=` and not `IFS=$'\n'` `IFS=` sets the separator to the empty string, which disables word splitting entirely for that command: there are no delimiter characters, so nothing is stripped and nothing is split, and the whole line lands in `line`. ```bash line=' hello world ' IFS= read -r a <<< "$line"; printf '[%s]\n' "$a" # [ hello world ] read -r b <<< "$line"; printf '[%s]\n' "$b" # [hello world] ``` With the default `IFS`, the leading and trailing spaces are gone. For a config file, a diff, a Markdown file or anything where indentation is data, that is silent corruption. Equally important is the **shape** of the assignment. `IFS= read -r line` is a *variable assignment prefixing a command*, so the value applies only to that command's environment; after `read` returns, `IFS` still has its original value. Compare `IFS=; read -r line`, with a semicolon, which changes `IFS` for the rest of the script and will quietly break unrelated code further down. ## Why `-r` Without `-r`, `read` interprets backslash as an escape: ```bash IFS= read -r p <<< 'a\tb'; printf '[%s]\n' "$p" # [a\tb] IFS= read q <<< 'a\tb'; printf '[%s]\n' "$q" # [atb] ``` Note what the non-`-r` version did: it did **not** turn `\t` into a tab; it removed the backslash and kept the `t`. Backslash here means "the next character is literal", not "C escape sequence". Windows paths, regexes, LaTeX, and any file containing `\` are mangled. Worse, a line whose final character is a backslash is treated as a line continuation and glued to the next line, so your loop silently sees fewer iterations than the file has lines. `-r` is correct for essentially every scripted use; there is no realistic reason to omit it when reading data. ## Where the input comes from `done < input.txt` attaches the file to the loop's standard input, so each `read` takes the next line. The loop terminates when `read` returns a non-zero status, which happens at end of input. One edge case is worth knowing: if the file's final line has no trailing newline, `read` assigns the partial line to `line` **and** returns non-zero, so the `while` condition is false and that last line is never processed. The standard defence is: ```bash while IFS= read -r line || [[ -n $line ]]; do printf '%s\n' "$line" done < input.txt ``` ## Why not `for line in $(cat file)` That alternative fails on this leaf's core mechanism: the command's output is subject to word splitting, so you iterate over *words*, not lines — every space becomes an iteration boundary — and each word is then glob-expanded, so a line containing `*` explodes into filenames. It also buffers the whole file into memory. `while IFS= read -r line` iterates over exactly the lines, streams, and is the idiom an interviewer expects to hear. Remember the whole phrase as one unit with a meaning per token: `IFS=` = do not trim or split; `read` = one line at a time; `-r` = backslashes are data; `< file` = where the lines come from.

  • Why write `IFS= read` rather than `IFS=; read` on the previous line?
    `IFS= read` is a variable assignment prefixing a command, so the value is scoped to that single invocation and `IFS` is untouched afterwards. `IFS=;` is an ordinary assignment that changes `IFS` for the rest of the script, silently altering how every later unquoted expansion splits — a bug that surfaces far from the line that caused it.
  • Your loop processes every line except the last one. What is the likely cause?
    The file's final line has no trailing newline. `read` still assigns the partial line to the variable, but it returns non-zero at end of input, so the `while` condition fails and the body never runs for it. Use `while IFS= read -r line || [[ -n $line ]]` so a non-empty leftover is still processed.
  • How would you read lines produced by `find` when filenames may contain newlines?
    Newline-delimited input cannot represent such names, so switch delimiters: emit NUL-separated records with `find ... -print0` and read them with `read -r -d ''`. The `-d ''` option makes `read` stop at a NUL byte instead of a newline, which is the only byte a POSIX filename cannot contain.

saying these in an interview costs you the question

  • Thinks read hands back the raw line by default
  • Believes -r makes read handle regular expressions
  • Says without -r a backslash-t becomes a real tab
  • Treats IFS= as a global change instead of a per-command prefix
  • Uses for line in $(cat file) as an equivalent idiom

context