skip to content

A reviewer on your team insists that every expansion in a bash script be double-quoted, and a colleague objects that some of them cannot possibly break. In which bash contexts is an unquoted expansion genuinely safe, and would you still require the quotes there?

level: seniorimportance: should knowfreq 35%

answer

  1. a short list of non-splitting contexts
  2. assignment right-hand side is one of them
  3. redirection targets are not on the list
  4. mechanics and policy give different answers
  5. wanting splitting means wanting an array

basics

~20 s

Four contexts perform no word splitting or globbing: the right-hand side of a simple assignment, inside [[ ]], the word after case, and arithmetic contexts such as (( )). Everywhere else quoting is mandatory — and most teams still quote unconditionally, because the exceptions are a memory test.

solid answer

~50 s

The colleague is technically right about a short list. Bash performs no word splitting or pathname expansion on the right-hand side of a simple assignment (`x=$y`), on expansions inside `[[ ]]`, on the word after `case`, or in arithmetic contexts like `(( n = $count + 1 ))`. Everywhere else — command arguments above all — an unquoted expansion is split on `IFS` and then globbed, and redirection targets are worse than most people expect: `> $file` with a value containing a space fails with `ambiguous redirect` rather than doing something sensible. I would still require the quotes. The exception list is knowledge that has to be re-derived by every reader and every reviewer, code gets moved from a `[[ ]]` into a command argument during refactoring, and a blanket rule is enforceable by ShellCheck's `SC2086` in CI. The genuine case for leaving an expansion unquoted is when you actually want it split into multiple words — and that is a signal to use an array instead.

code

bash · 8 lines
bash
y='a b'

x=$y                                  # safe: assignment RHS is not split
[[ $y == 'a b' ]] && echo 'safe in [[ ]]'
case $y in 'a b') echo 'safe as the case word' ;; esac
(( n = ${#y} + 1 )); echo "$n"        # safe: arithmetic context

( echo hi > $y ) 2>/dev/null || echo 'NOT safe: ambiguous redirect'

go deeper

for a junior

Take the simple rule away: quote every expansion. You are not expected to recite the exceptions, and following the blanket rule will never make your script wrong.

for a middle

Name the non-splitting contexts — assignment right-hand side, [[ ]], the case word, arithmetic — and contrast them with redirection targets and command arguments, where splitting very much applies.

for a senior

Show that mechanics and policy are different questions: you know the safe list, and you still enforce quoting because refactoring moves expansions between contexts and the failure only appears on values you did not test.

for a principal

Own the enforcement story — one rule with no exceptions, SC2086 failing the build, arrays as the answer whenever someone wants splitting, and a standard that a reviewer can apply without re-deriving shell grammar.

## The contexts that do not split Bash's word splitting and pathname expansion apply when an expansion appears where a *list of words* is expected. In a handful of places the grammar expects exactly one word, and bash skips both stages there. **Right-hand side of a simple assignment.** `x=$y` never splits, whatever `y` contains: ```bash y='a b' x=$y # x is 'a b' - one value, no splitting, no globbing ``` **Inside `[[ ]]`.** The conditional keyword performs no word splitting or globbing on expansions: ```bash f='My Report.txt' [[ $f == 'My Report.txt' ]] && echo match # true, despite the space [[ -f $f ]] && echo exists # safe unquoted too ``` Remember the asymmetry from pattern matching: the *right* side of `==` is a pattern, so leaving a variable unquoted there lets its contents act as pattern syntax even though it will not be split. **The `case` word.** `case $f in ... esac` is safe for the same reason. **Arithmetic contexts.** Inside `(( ))` and `$(( ))` the contents are evaluated as an arithmetic expression, not as words, so splitting is irrelevant. (Untrusted input in an arithmetic context is a separate hazard entirely and does not belong to quoting.) That is the list. Everything else splits. ## The contexts people wrongly assume are safe **Redirection targets.** Bash does expand and split the word after a redirection operator, and if it yields more than one word it aborts: ```bash f='a b.txt' echo hi > $f # bash: $f: ambiguous redirect - nothing is written echo hi > "$f" # correct ``` The failure is loud, which is a mercy, but a script that ignores exit statuses will silently produce no output file. **Command arguments.** The overwhelming majority of expansions in a real script are command arguments, and every one of them splits and globs. This includes arguments to builtins like `echo` and `[`, and the old `[ ]` test command in particular — `[ -f $f ]` really does break on a space, unlike the `[[ ]]` form. **Anything in a here-string or a command substitution's inner command** follows normal rules, since those are ordinary command contexts. ## Why the blanket rule wins anyway The safe list is real, and it is still the wrong thing to optimise for. *It is a memory test with an asymmetric payoff.* Getting it right saves two characters; getting it wrong ships a bug that fires only on a filename with a space or an empty value. Nobody has ever been paged because a variable was quoted unnecessarily. *Code moves.* A condition written as `[[ $target == prod ]]` is safe today. When someone extracts it into `check_target $target`, the safety evaporates and the diff looks innocuous, because the expansion itself did not change — only its surroundings did. *A blanket rule is machine-enforceable.* ShellCheck reports unquoted expansions as `SC2086` and, notably, does not fire in the contexts listed above — so the linter already understands the exceptions and a clean run does not require you to. What a blanket team rule buys is that reviewers never argue about it and no one has to whitelist a warning. *Reviewers read for anomalies.* When every expansion is quoted, an unquoted one stands out as a deliberate statement, and the reader knows to check whether splitting was intended. ## When unquoted is deliberate There is one honest reason to leave an expansion unquoted: you want the value split into several words. The classic shape is a variable holding flags: ```bash opts='--verbose --dry-run' mytool $opts # deliberately splits into two arguments ``` This works until an option value contains a space, at which point it silently breaks. The correct construction stores the flags in an array and expands that instead, which preserves each element's boundaries — the same argument-boundary problem that `"$@"` solves for a script's own parameters. If you find yourself relying on splitting, treat it as a design signal rather than an idiom to defend, and if you truly must, write a comment saying the splitting is intentional so the next reviewer does not "fix" it. ## How to answer this in an interview Give the short safe list to show you know the mechanics, then give the policy answer and say why it does not follow from the mechanics: the cost of an unnecessary quote is zero, the cost of a missing one is a production bug on a value you did not anticipate, and lint enforcement only works with a rule that has no exceptions to argue about.

  • What actually happens with `echo hi > $file` when the value contains a space?
    Bash expands and splits the redirection word, gets two words, and refuses with `ambiguous redirect` — no file is created and the command fails. It is a loud failure rather than a silently wrong one, but a script that does not check exit statuses just produces no output. Quote the target.
  • Your colleague points out that ShellCheck does not warn about `[[ $f == x ]]`. Does that settle it?
    It confirms the mechanics — ShellCheck models the non-splitting contexts and stays quiet there — but not the policy. A team rule of 'always quote' needs no exceptions to be taught, survives refactoring that moves an expansion into a command argument, and makes an unquoted expansion read as a deliberate choice.
  • How would you handle a variable that is supposed to hold several command-line flags?
    Store them in an array and expand it quoted, so each element stays one argument regardless of spaces. A single string relying on word splitting works only while no flag value contains whitespace, and it fails silently the day one does. If the script must accept flags as a single string from an environment variable, parse it explicitly rather than letting IFS do it.

saying these in an interview costs you the question

  • Claims every unquoted expansion is a bug, with no mechanism
  • Says redirection targets are safe unquoted
  • Thinks quoting inside [[ ]] changes the comparison in every case
  • Defends a flags-in-a-string variable as idiomatic
  • Treats a clean ShellCheck run as proof the style question is settled

context