skip to content

A colleague adds `IFS=$'\n\t'` near the top of a shared bash script, saying it makes the script safer. What does that global change actually buy, what does it fail to protect against, and what can it break?

level: seniorimportance: should knowfreq 38%

answer

  1. narrowing the separator set
  2. space stops being a delimiter
  3. globbing is left untouched
  4. sourced code assumes the default
  5. local beats global; quotes beat both

basics

~20 s

It removes the space from the separator set, so unquoted expansions no longer split on spaces — only on newlines and tabs. It does not stop globbing, does not make quoting unnecessary, and silently changes any code that relied on space splitting, including sourced libraries.

solid answer

~50 s

Setting `IFS=$'\n\t'` narrows word splitting to newlines and tabs, so an unquoted `$file` holding `my report.txt` stays one word. That is a genuine mitigation for the most common filename bug, and it is why the line travels with the popular strict-mode preamble. But it is a partial one: the second half of an unquoted expansion, pathname expansion, still runs, so a value containing `*` still explodes; a filename containing a tab or a newline still splits; and `"$@"`-style quoting is still the actual fix. The cost is that `IFS` is a global affecting every later expansion in the script and everything it sources, so any code that deliberately splits on spaces — `cmd $flags`, a `for x in $list` over a space-separated list, a helper library written against the default — silently changes behaviour. I would rather quote everything, scope `IFS` per command, and treat a global change as a last resort in a script I fully own.

code

bash · 9 lines
bash
IFS=$'\n\t'
file="my report.txt"
printf '[%s]\n' $file    # [my report.txt] - no longer split on the space

v='*'
printf '[%s]\n' $v       # still expands to every file: IFS does not stop globbing

flags="-v --color=never"
printf '[%s]\n' $flags   # [-v --color=never] - deliberate splitting now broken

go deeper

for a junior

Recognise the line when you see it at the top of a script and know it means unquoted variables no longer split on spaces. Do not treat it as permission to drop quotes.

for a middle

Explain that IFS controls only the splitting step, so pathname expansion still runs, and that the assignment is a global affecting everything the script later sources or expands.

for a senior

Weigh it as a tradeoff: a real mitigation for the common case, blind to globs and to tabs and newlines in filenames, and a silent behaviour change for any code that relied on space splitting. Prefer per-command or local scoping.

for a principal

Own the convention for the codebase — where global shell state may be set at all, why libraries must never touch it, and how quoting plus linting in CI is the durable control that a defensive IFS only supplements.

## What the change does `IFS=$'\n\t'` uses ANSI-C quoting to build a two-character string: a newline and a tab. Assigning it means those are the only characters bash will split unquoted expansions on. The space — by far the most common character inside a filename, a title, a user's full name or a path on macOS — stops being a delimiter. ```bash IFS=$'\n\t' file="my report.txt" printf '[%s]\n' $file # [my report.txt] - one word now ``` That is why the line appears alongside `set -euo pipefail` in the widely copied "unofficial strict mode" preamble. Its logic is defensive: if someone forgets a pair of quotes, the most likely disaster is downgraded. ## What it does not buy Three gaps matter, and being able to name them is the point of the question. **Globbing still happens.** Word splitting is only the first of the two steps applied to an unquoted expansion. The second, pathname expansion, is untouched by `IFS`: ```bash IFS=$'\n\t' v='*' printf '[%s]\n' $v # still lists every file in the directory ``` **Newlines and tabs are legal in filenames.** On a POSIX filesystem the only bytes a filename cannot contain are `/` and NUL. A file created by an untrusted process — an upload directory, an extracted archive — can carry a newline precisely to be split by a script like this one. Narrowing `IFS` chooses which hostile input still works; it does not eliminate the class. **Quoting is still required.** `IFS` changes what happens when you *forget*; it is not a substitute for the habit. ShellCheck still flags SC2086, correctly, because the linter cannot know what `IFS` will be at runtime — and that is itself a hint about how hard this is to reason about locally. ## What it can break `IFS` is a global that reaches every later expansion in the process, and shells compose by inclusion. The failure modes: - **Deliberate splitting stops working.** `flags="-v --color=never"; cmd $flags` was passing two arguments and now passes one long argument containing a space. The command fails with a confusing message far from the assignment. - **Loops over space-separated lists collapse.** `for host in $HOSTS` becomes a single iteration with the whole list as one value. - **Sourced libraries change behaviour.** A shared helpers file written against the default `IFS` inherits your value, and its author never tested it. This is the nastiest version because the broken code is not in the file that made the change. - **`IFS` is also a joiner.** Some expansions use its *first character* to join elements, so a newline-first `IFS` silently alters strings those produce. Because of this reach, changing `IFS` globally in a library, a sourced snippet or a shell function that runs in the caller's context is close to indefensible. In a self-contained script whose entire body you control, it is a reasonable belt-and-braces measure — provided the team knows it is there. ## The alternatives, in order of preference 1. **Quote every expansion** and use arrays for lists. This is the real fix; the other measures only reduce the damage when it is skipped. 2. **Scope the change to one command**: `IFS=, read -ra parts <<< "$row"`. `IFS` is restored the moment the command returns. 3. **Scope it to a function** with `local IFS=$'\n'`, so it is restored when the function returns even on an early exit path — and it does not leak to the caller. 4. **Scope it to a subshell**: `( IFS=,; ... )`, when several commands need it and you accept the process boundary. 5. **Save and restore manually** — `old_IFS=$IFS; ...; IFS=$old_IFS` — only when nothing above fits. It is fragile: any early `return`, `exit` or error path skips the restore. ## Answering it in an interview Say what it buys, then immediately show you know it is partial rather than magic, then land on scope: local beats global, and quoting beats both. Adding "and it should be documented at the top of the script, because a reader who does not see it will misread every unquoted expansion below" is the senior note — the real cost is to the next person reading the code, not to the shell.

  • If a function needs a different IFS for several commands, what is the safest way to scope it?
    Declare `local IFS=$'\n'` inside the function. The value is restored when the function returns, including on an early `return` or an error path, and it never leaks to the caller. A subshell `( IFS=,; ... )` is the alternative when you also want the rest of the state isolated, at the cost of a process and of losing any variables set inside.
  • Does setting IFS=$'\n\t' remove the need to quote expansions?
    No. It only narrows which characters split, and it leaves pathname expansion entirely alone, so an unquoted value containing `*` still expands against the directory. Filenames may also contain tabs and newlines. Quoting is the fix; a narrowed IFS is at most a safety net for the times someone forgets.
  • Someone proposes putting this line in a shared library that other scripts source. What is your response?
    Reject it. A sourced file runs in the caller's shell, so it would silently change how every expansion in every consumer script splits, including code its author never saw. Global shell state belongs to the top-level script that owns the process; libraries should scope changes with `local` inside their functions.

saying these in an interview costs you the question

  • Says a narrowed IFS makes quoting unnecessary
  • Believes it also disables globbing of unquoted expansions
  • Assumes filenames cannot contain tabs or newlines
  • Sets IFS globally inside a sourced library or function
  • Relies on manual save-and-restore across code paths that can exit early

context