skip to content

Word Splitting and IFS

An unquoted $var is not one argument — the shell splits it on IFS and then globs each piece. Once you can narrate that sequence you can explain filenames with spaces, empty-variable bugs, and why `while IFS= read -r line` is written in exactly that way.

part ofBashoverview, primer and where to startread it →
on this pageshow

questions

5

In a bash script, `files="report 2024.txt"` followed by `rm $files` deletes the wrong things. What exactly does bash do to the unquoted `$files` before `rm` starts, and how do you fix it?

level: middleimportance: must knowfreq 80%

answer

  1. one variable, several arguments
  2. IFS decides the boundaries
  3. split first, then glob each piece
  4. quotes at assignment do not survive
  5. ShellCheck SC2086

basics

~20 s

Bash splits the expanded value into words at the characters in IFS (space, tab and newline by default), then pathname-expands each word, so rm receives two arguments, report and 2024.txt. Quoting it as "$files" passes one.

solid answer

~50 s

An unquoted expansion is not one argument. After bash substitutes the value of `files`, it applies two more steps to the result: word splitting on the characters in `IFS` — space, tab and newline by default — and then pathname expansion on each word that splitting produced. `report 2024.txt` becomes the two words `report` and `2024.txt`, so `rm` is invoked with two arguments and deletes the wrong file. The quotes in the assignment are long gone by that point; quoting is not a property that travels with a value, it is something you do at the point of use. The fix is `rm -- "$files"`. If the variable is genuinely meant to hold several paths, a whitespace-delimited string is the wrong container — an array is, because a string cannot encode an element containing a space. ShellCheck reports this as SC2086.

code

bash · 6 lines
bash
files="report 2024.txt"
printf '[%s]\n' $files     # [report] and [2024.txt] - two words
printf '[%s]\n' "$files"   # [report 2024.txt] - one word

v="* one.txt"
printf '[%s]\n' $v         # splitting yields *, which then globs the directory

go deeper

for a junior

Be ready to state the rule and apply it: quote every expansion you pass as an argument, so rm "$files" rather than rm $files. Know that a value holding a space becomes two arguments without quotes.

for a middle

Explain the mechanics in order — the value is split on the characters in IFS, then each resulting word is glob-expanded — and name the default IFS as space, tab and newline. Show that quoting at assignment time does not persist.

for a senior

Show the production judgment: audit scripts for unquoted expansions with ShellCheck SC2086, treat any value that came from a filename, a file or an argument as hostile, and reach for arrays instead of space-joined strings when something is genuinely a list.

for a principal

Own the convention: an always-quote rule plus an array-for-lists rule enforced by a linter in CI is what actually keeps this class of bug out of a codebase, and be able to argue when a script's data model has outgrown whitespace-delimited strings entirely.

## The two extra steps on an unquoted expansion When bash substitutes a parameter (`$var`, `${var}`), the text it produces is **not automatically one word**. Unless the expansion appeared inside double quotes, bash applies two further steps to the result: 1. **Word splitting** — the result is chopped into words at the characters listed in the shell variable `IFS`. 2. **Pathname expansion (globbing)** — each word produced by step 1 is then treated as a glob pattern and, if it matches files, replaced by the matching names. Only after both steps does bash know how many arguments the command gets. Everything below follows from that ordering. ## What IFS is `IFS` stands for *Internal Field Separator*: the shell variable holding the delimiter characters used for splitting. Its default value is space, tab and newline, which you can write literally as `IFS=$' \t\n'`. Splitting obeys two different rules depending on the character: - **Whitespace delimiters** (space, tab, newline) collapse: a run of them is one delimiter, and leading or trailing runs are ignored. `" a b "` splits into exactly two words. - **Non-whitespace delimiters** (a comma, a colon) delimit one field each, so repeats produce *empty* fields. With the default `IFS`, splitting therefore never produces empty words — which is why an empty variable expands to nothing at all rather than to one empty argument. ## The example, step by step ```bash files="report 2024.txt" printf '[%s]\n' $files # [report] then [2024.txt] -> two words printf '[%s]\n' "$files" # [report 2024.txt] -> one word ``` Bash expands `$files` to the eight-plus characters `report 2024.txt`, splits at the space into `report` and `2024.txt`, globs each (neither contains a glob character, so both survive unchanged), and hands `rm` two arguments. The single quotes around the assignment mattered only while bash was parsing the assignment line: they stopped the space from splitting *that* line. They are not stored in the variable and cannot protect a later expansion. ## Globbing runs after splitting, on the pieces The second step surprises people because it means a value can grow: ```bash v="* one.txt" printf '[%s]\n' $v # every file in the directory, then [one.txt] ``` Splitting produces the words `*` and `one.txt`; the `*` is then a glob and expands to the directory listing. A value read from a file, a filename, or a CLI argument can therefore inject both extra arguments and a directory listing into your command line. This is the mechanism behind the classic `for f in $(ls)` bug: any name containing a space becomes two loop iterations, and a name containing `*` or `?` gets glob-expanded again. ## What quoting does Double quotes suppress both steps: the expansion becomes exactly one word, whatever whitespace or glob characters it contains. That is the whole reason the house rule "quote every expansion unless you have a specific reason not to" exists. `"$files"`, `"$1"`, `"$(command)"`, `"${arr[@]}"` — quoting is applied where the value is *used*. A few practical consequences: - `rm -- "$files"` also needs the `--`, because a filename that begins with `-` would otherwise be read as an option; quoting fixes splitting, not option parsing. - If a variable really holds a list, use an array so each element keeps its own identity; a space-joined string is lossy for names containing spaces. - Deliberate splitting is legitimate but rare: `cmd $flags` where `flags="-v --color=never"` works precisely *because* the value is split. Reviewers cannot tell intentional splitting from a bug, so annotate it or, better, use an array. ## Getting it caught for you ShellCheck's SC2086 ("Double quote to prevent globbing and word splitting") fires on every unquoted expansion in an argument position, and it is the single highest-value warning in shell linting. In an interview, being able to narrate *split, then glob, then run* — in that order, with `IFS` named as the thing that decides where the splits happen — is the answer being probed for.

  • The value was quoted when it was assigned — why doesn't that protect it later?
    Quotes are syntax consumed while bash parses that assignment line, not data stored in the variable. They stopped the space from splitting the assignment itself; the variable holds the bare characters `report 2024.txt`. Splitting is decided fresh at each point of use, so protection has to be applied there: `"$files"`.
  • When is leaving an expansion unquoted actually the right choice?
    When you want the splitting — for example `cmd $flags` where `flags` deliberately holds several space-separated options, or a variable you know holds a glob pattern you intend to expand. It is rare and unreadable, since a reviewer cannot distinguish intent from a bug, so an array of arguments is almost always the better expression.
  • Does quoting also protect against a filename that starts with a dash?
    No. Quoting controls word splitting and globbing only; the resulting single word is still passed to the command, which may interpret a leading `-` as an option. Guard that separately with the `--` end-of-options marker, or by prefixing a relative path with `./`.

saying these in an interview costs you the question

  • Thinks quotes used at assignment stay attached to the value
  • Says a variable always expands to exactly one argument
  • Believes globbing happens before word splitting
  • Claims globs inside a variable's value are never expanded
  • Fixes it by escaping spaces inside the assignment instead of quoting the use

context

open as a page

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%

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.

open as a page

In a bash script you must split each line of a colon-delimited file into fields. Explain how `IFS=: read -ra fields <<< "$line"` works, what scope the `IFS=:` has, and what `read -ra` produces for the line `x::y`.

level: middleimportance: should knowfreq 52%

basics

~20 s

The IFS=: prefix applies to that single read, leaving the script's IFS untouched. read -ra splits the input on colons and assigns the fields to array elements. Because a colon is not whitespace, x::y yields three fields: x, an empty string, and y.

open as a page

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%

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.

open as a page

In bash with `v=''`, the function `count() { echo $#; }` prints different numbers for `count $v` and `count "$v"`. What does each print, and why?

level: juniorimportance: nice to knowfreq 42%

basics

~20 s

count $v prints 0 and count "$v" prints 1. An unquoted empty expansion is split on IFS into zero words, so no argument is passed at all, while double quotes force it to become exactly one argument, an empty string.

open as a page