In zsh, `files="a.txt b.txt"; touch $files` creates one file literally named `a.txt b.txt`, while bash creates two files. Why do the shells differ, and what are the correct ways to get the two-file behaviour in zsh?
answer
- a default that removed a bug class
- IFS is not applied where you expect
- one word, not several
- the option name starts with SH_
- the data type was wrong all along
basics
~20 szsh does not split unquoted parameter expansions into words on whitespace the way bash does, so $files stays a single argument. Use a real array — files=(a.txt b.txt) — or force splitting for one expansion with ${=files}.
solid answer
~40 sbash performs IFS word splitting on any unquoted parameter expansion, so `$files` becomes two arguments. zsh deliberately does not: an unquoted scalar expands to exactly one word, which is why `touch` sees a single filename containing a space. The bash behaviour is available three ways in zsh — `setopt SH_WORD_SPLIT` turns it on shell-wide, `${=files}` forces splitting for one expansion, and `emulate sh` enables it as part of sh compatibility. None of those is the right answer, though: if you want multiple values, use an array — `files=(a.txt b.txt); touch $files` — because an unquoted array in zsh expands to one word per element with no quoting hazards. Note the asymmetry: zsh still field-splits unquoted *command substitution*, so `touch $(cat list.txt)` does split.
code
bash · 10 lines# run under zsh
files="a.txt b.txt"
print -l $files # 1 line: "a.txt b.txt"
print -l ${=files} # 2 lines: forced IFS splitting
arr=(a.txt "b c.txt")
print -l $arr # 2 lines; the embedded space survives
print -l "${arr[@]}" # same, and this spelling also works in bash
print -l $(printf 'a\nb\n') # 2 lines: command substitution IS splitgo deeper
Remember that in zsh an unquoted variable holding "a b" stays one argument, and that a list of values belongs in an array written files=(a b), not in a space-separated string.
Explain field splitting as a distinct expansion stage governed by IFS, and name the three zsh escape hatches — ${=var}, setopt SH_WORD_SPLIT, and sh emulation — while arguing for an array instead.
Show the migration risk concretely: shared dotfiles or helper functions whose meaning changes when sourced by the other shell, plus the command-substitution asymmetry that makes two apparently identical loops behave differently.
Take a position on the tradeoff: zsh eliminated the most common shell bug class at the cost of silently changing the meaning of copied code, and say how you would keep a team's shared shell code in the portable subset rather than betting on either default.
## What word splitting is In a POSIX shell, expansion happens in stages. After a parameter like `$files` is replaced by its value, the result is broken into separate words at every run of characters found in `IFS` (by default space, tab, newline). That second stage is *field splitting*, or word splitting. It is why `files="a.txt b.txt"; touch $files` creates two files in bash: the shell hands `touch` two arguments, not one. It is also the single largest source of bugs in shell code. A path containing a space, a filename from `find`, a variable holding a person's name — every one of them silently becomes several arguments unless the author remembered to write `"$files"`. This is why every shell style guide tells you to quote every expansion. ## What zsh does instead zsh's designers took the opposite default. An unquoted parameter expansion produces exactly **one** word regardless of the whitespace inside it, so `$files` and `"$files"` behave the same for a scalar. Your `touch` therefore creates one file whose name contains a space. The practical effect is that the quoting bug class largely disappears in interactive zsh. The cost is portability confusion: code copied from bash, or written by someone with bash reflexes, quietly changes meaning. ## The three ways to get splitting back ```zsh files="a.txt b.txt" touch ${=files} # split this one expansion on IFS setopt SH_WORD_SPLIT # shell-wide bash behaviour touch $files # now creates two files unsetopt SH_WORD_SPLIT emulate -L sh # sh emulation turns SH_WORD_SPLIT on among others ``` `${=name}` is zsh's per-expansion splitting flag; its mirror image is `${~name}`, which turns on *globbing* of the expansion's result (zsh does not glob expansion results by default either — bash does not glob them either, but bash's `GLOBSUBST`-like behaviour differs in detail, so treat `${~name}` as the deliberate opt-in). `setopt SH_WORD_SPLIT` is what `emulate sh` sets for you. ## The answer an interviewer wants: use an array All three switches are workarounds for using the wrong data type. A string holding several values is a list pretending to be a scalar. zsh has had arrays since long before bash did: ```zsh files=(a.txt "b c.txt") touch $files # two arguments, and the space in the second is preserved print ${#files} # 2 ``` An unquoted array in zsh expands to one word per element, and — crucially — elements containing spaces stay intact. That is the property no amount of `IFS` fiddling gives you in bash. The portable spelling that means the same thing in both shells is `"${files[@]}"`. ## The asymmetry that catches people zsh's no-splitting rule applies to **parameter expansion**. Unquoted **command substitution** is still field-split on IFS: ```zsh out=$(printf 'a\nb\n') print -l $out # one word: a and b on separate lines but a single argument print -l $(printf 'a\nb\n') # two words ``` So `for f in $(ls)` behaves in zsh much as it does in bash — badly, for filenames with spaces — while `list=$(ls); for f in $list` does not. If you are debugging why one loop splits and a seemingly identical one does not, this asymmetry is usually the reason. `IFS` itself still exists in zsh and still governs both this splitting and the `read` builtin. ## Why this shows up in interviews It is the cleanest example of a shell changing a default for safety and breaking compatibility as a result. A candidate who can say "zsh removed implicit word splitting because unquoted expansions are the classic shell bug, and the migration cost is that code assuming splitting silently changes behaviour" has understood the whole leaf. A candidate who says "zsh is broken" has not. A final practical note for migrating shared code: rather than sprinkling `${=var}` through a ported file, decide whether each variable is a scalar or a list. If it is a list, make it an array. If the file must be sourced by both bash and zsh, keep it to `"${arr[@]}"` and explicit quoting, which mean the same thing everywhere.
- If zsh does not split unquoted expansions, is quoting them pointless in zsh?No. Quoting still matters for empty values, for suppressing globbing when GLOB_SUBST or `${~var}` is in play, and above all for portability — the moment the file is sourced by bash or run under `emulate sh`, unquoted expansions split again. Quoting also documents intent, so reviewers do not have to know which shell rules apply.
- What does `${~var}` do, and how does it relate to `${=var}`?`${=var}` forces IFS word splitting on that one expansion; `${~var}` forces filename generation on it, so a value like `*.txt` expands to matching files instead of staying literal. They are independent per-expansion opt-ins to behaviour zsh does not perform by default, and they can be combined.
- How would you write a helper that takes a variable number of arguments and works identically in bash and zsh?Use an array and always expand it as `"${args[@]}"`, and quote every scalar expansion. Avoid bare `$args`, since it means the first element in bash and every element in zsh, and avoid relying on IFS splitting of a space-separated string, since that is exactly the behaviour the two shells disagree about.
saying these in an interview costs you the question
- Claiming zsh ignores IFS entirely
- Thinking quoting is unnecessary in zsh so portability does not matter
- Reaching for setopt SH_WORD_SPLIT instead of using an array
- Believing command substitution is also unsplit in zsh
- Saying bash and zsh differ only in prompt and completion features