skip to content

A team standardises on `[[ ]]` for every bash script and stops requiring quotes inside conditionals, on the grounds that `[[ ]]` makes quoting unnecessary. Which failure modes does that genuinely remove, and where does dropping the quotes still bite?

level: seniorimportance: should knowfreq 40%

answer

  1. only the operands are protected
  2. the right side is still a pattern
  3. -eq quietly does arithmetic
  4. quote when you mean equality
  5. the habit stops at the brackets

basics

~20 s

[[ ]] stops word splitting and globbing of its operands, so unquoted file and string tests really are safe. It does not make the right side of ==, != or =~ literal, does not validate the operands of -eq, and the unquoted habit breaks the moment you leave the brackets.

solid answer

~50 s

`[[ ]]` is parsed by the shell, so expansions inside it are never word-split or pathname-expanded: `[[ -f $file ]]` and `[[ -z $x ]]` really are safe with spaces, newlines or an empty value, and that is a genuine win over `[ ]`. What it does **not** do is make the *right* side of `==`, `!=` or `=~` literal — unquoted, that side is a pattern, so `[[ $user == $allowed ]]` with `allowed='a*'` matches `admin`. That is an authorization bug, not a syntax error, and nothing is printed. It does not sanity-check `-eq` operands either: they go through arithmetic evaluation, so `[[ $x -eq 0 ]]` is true when `$x` is empty, unset, or a bare word. And the habit does not travel — the same unquoted variable in a command argument still splits. Use `[[ ]]`, and keep quoting.

code

bash · 6 lines
bash
allowed='a*'
user=admin
[[ $user == $allowed ]]   && echo "unquoted right side: pattern lets admin through"
[[ $user == "$allowed" ]] || echo "quoted right side: literal compare rejects admin"
x=
[[ $x -eq 0 ]] && echo "-eq evaluates arithmetically: empty counts as 0"

go deeper

for a junior

Take away the simple rule: quote your variables, even inside [[ ]]. It is never wrong, and it keeps the habit intact everywhere else in the script.

for a middle

Explain precisely which expansion steps [[ ]] suppresses — word splitting and pathname expansion on the operands — and why that says nothing about the pattern semantics of == and =~.

for a senior

Show the production shape of the gap: an allow-list check where a wildcard in configuration silently widens access, and a numeric guard that accepts an unset variable as zero. Say how you would test for both.

for a principal

Own how the standard is written and enforced. "No quotes needed in conditionals" trains the wrong generalisation; state the rule as a property of the construct, note that linting cannot cover the semantic gaps, and decide where review or tests must.

## What the keyword actually guarantees The rule "`[[ ]]` means you do not have to quote" is half true, and the half that is false is the dangerous half. The true half: `[[` is a shell reserved word, so the shell parses the whole construct and evaluates the operands internally instead of building an argument list for a command. Two expansion steps that apply to ordinary commands therefore do not happen inside it — **word splitting** and **pathname expansion**. That is real: ```bash f="my file.txt" [[ -f $f ]] # correct: one operand, spaces and all [ -f $f ] # [: my: binary operator expected x="" [[ -z $x ]] # correct: true [ -n $x ] # true! test sees one argument, "-n", which is non-empty ``` The entire family of "unary operator expected" and "too many arguments" failures disappears, along with the working-directory-dependent glob accidents. If that were the whole story, the team's rule would be fine. ## Gap 1: the right-hand side is still a pattern `==`, `!=` and `=~` treat their right operand as a pattern or a regex while it is unquoted. Splitting and globbing were suppressed; *matching* was not — it is the operator's own semantics. ```bash allowed='a*' user=admin [[ $user == $allowed ]] # TRUE — pattern match, not equality [[ $user == "$allowed" ]] # false — literal comparison ``` This is the worst kind of bug: it appears in authorization checks, allow-lists and confirmation prompts, wherever a value from configuration or from an argument reaches the right-hand side. Nothing errors, nothing logs, and the condition is simply more permissive than it reads. A team rule that says "no quotes needed in conditionals" removes the one visual cue that would have distinguished "compare" from "match". The converse bites too: a quoted regex matches literally and the branch is silently dead. ## Gap 2: `-eq` evaluates arithmetic, it does not validate Inside `[[ ]]` the numeric operators put their operands through arithmetic evaluation. In that context a bare word is a **variable name**, and an unset or empty variable is **0**: ```bash x="" [[ $x -eq 0 ]] # true [[ abc -eq 0 ]] # true — abc is an unset variable, therefore 0 ``` So a guard like `if [[ $count -eq 0 ]]; then skip; fi` treats "the variable was never set" and "the command produced nothing" as a legitimate zero. `[ "$x" -eq 0 ]` at least fails loudly with `integer expression expected` — the older construct is stricter here. If the value came from outside the script, validate its shape first, for instance with a regex match, rather than trusting a numeric operator to reject it. ## Gap 3: the habit does not travel Quoting is a whole-script discipline, and `[[ ]]` is an island. The same author who learned that `$file` is safe inside the brackets writes: ```bash [[ -f $file ]] && cp $file $dest # the cp splits on spaces for f in $files; do ... # splits and globs rm $tmpdir/* # nothing to do with conditionals ``` The conditional was the *only* place the exemption applied. A rule phrased as "you do not need quotes" trains the wrong generalisation; a rule phrased as "`[[ ]]` suppresses splitting on its operands, and we quote anyway" trains the right one. And the moment a script needs a POSIX `#!/bin/sh` interpreter, `[[` is not available at all and every unquoted expansion in the file is live again. ## Why the linter will not save you ShellCheck's unquoted-expansion warning SC2086 is deliberately **not** emitted inside `[[ ]]`, because splitting genuinely cannot happen there — the tool is right about the mechanism. It also cannot know whether an unquoted right operand of `==` was meant as a pattern; both readings are valid shell. So the two gaps above are precisely the ones static analysis is least able to flag, which makes them a review-discipline problem rather than a tooling problem. ## The policy that actually works Use `[[ ]]` in bash scripts — it is strictly safer than `[ ]` and more capable. Then keep three rules: 1. Quote by default anywhere, including inside `[[ ]]`; the cost is two characters. 2. Leaving the right side of `==` or `=~` unquoted is a **deliberate, commented** choice meaning "this is a pattern". 3. Never let a numeric operator serve as input validation; check the shape first. The useful framing in a review is that `[[ ]]` removes a class of *syntax* failures — noisy, immediate, easy to find — while the remaining risks are *semantic*: silent, permissive, and discovered in production.

  • How would you make the `[[ $user == $allowed ]]` check safe?
    Quote the right operand — `[[ $user == "$allowed" ]]` — so it is a literal comparison, and add a comment on the rare line where you deliberately want a pattern. Better still, stop comparing against a single configurable string: check membership in an explicit list of exact values, so a wildcard arriving from configuration cannot widen the check at all.
  • Would ShellCheck have caught either of these?
    No. Its unquoted-expansion warning SC2086 is intentionally not raised inside `[[ ]]`, because splitting cannot occur there, and no linter can know whether you meant the right operand as a pattern or as a literal — both are valid shell. These are semantic bugs, so they need a review rule and a test that feeds a wildcard through the check, not a tool setting.
  • Is there any case where `[ ]` is still the better choice in a bash script?
    Two. When the script must run under a POSIX shell, since `[[` is a keyword that dash and other /bin/sh implementations do not have. And when you want a numeric comparison to reject non-numeric input loudly: `[ "$x" -eq 0 ]` errors with "integer expression expected", where the `[[ ]]` form quietly evaluates it as arithmetic and calls it zero.

saying these in an interview costs you the question

  • Says [[ ]] makes quoting unnecessary everywhere
  • Believes == with an unquoted variable is plain equality
  • Trusts [[ $n -eq 0 ]] to validate numeric input
  • Assumes the linter would have flagged it
  • Carries the unquoted style into command arguments

context