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?
answer
- only the operands are protected
- the right side is still a pattern
- -eq quietly does arithmetic
- quote when you mean equality
- 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 linesallowed='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
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.
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 =~.
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.
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