skip to content

A bash script contains `if [ $answer = yes ]; then`. It works in testing but in production fails with `[: =: unary operator expected` or `[: too many arguments`. What is `[` really, why do those errors happen, and how does `[[ $answer = yes ]]` behave differently?

level: middleimportance: must knowfreq 82%

answer

  1. it is a command, not punctuation
  2. arguments are counted after expansion
  3. an empty unquoted expansion disappears
  4. the parser handles the keyword itself
  5. status 2 is not the same as false

basics

~20 s

In bash, [ is an ordinary command, not syntax: the shell word-splits and globs its arguments first, so an empty or multi-word $answer changes the argument count and test reports a usage error. [[ is a shell keyword, so the expansion stays one word and needs no quotes.

solid answer

~50 s

`[` is not syntax; it is the `test` builtin under another name, and it requires a final `]` argument. The shell expands `$answer` and splits it into words **before** test runs. With `answer` empty the command becomes `[ = yes ]`, so test sees `=` sitting where a unary operator belongs; with `answer='I guess'` it becomes `[ I guess = yes ]`, which is too many arguments. Both are usage errors that exit with status 2, so the `if` simply takes the else path and the script marches on with the wrong decision. Quoting fixes it: `[ "$answer" = yes ]` is always exactly three arguments. `[[ ]]` is a shell keyword the parser handles itself — expansions inside are neither word-split nor glob-expanded, so `[[ $answer = yes ]]` is safe unquoted, and it adds pattern matching, `=~` and `&&`/`||`.

code

bash · 6 lines
bash
answer=""
[ $answer = yes ]; echo "unquoted empty -> $?"
answer="I guess"
[ $answer = yes ]; echo "unquoted two words -> $?"
[ "$answer" = yes ]; echo "quoted -> $?"
[[ $answer = yes ]]; echo "keyword -> $?"

go deeper

for a junior

Be able to say that [ is a command and that the fix is quoting: [ "$answer" = yes ]. Recognise "unary operator expected" as the signature of an empty unquoted variable.

for a middle

Explain the mechanism: the shell expands and word-splits before test runs, so the argument count changes and test dispatches on that count. Then contrast it with [[ ]] as a keyword the parser evaluates itself.

for a senior

Show why this is an incident rather than a syntax nit: status 2 is swallowed by if, the message goes to stderr where nobody reads it, and the script takes a wrong branch quietly. Mention the glob variant whose answer depends on the working directory.

for a principal

Own the convention across a codebase: bash scripts use [[ ]] and quote anyway, POSIX-sh scripts quote unconditionally, and the linter enforces it in CI so the rule does not depend on reviewer attention.

## `[` is a command, and that explains everything The single most useful fact about `[` is that it is not part of the shell's grammar. It is the `test` builtin under a second name, with one extra rule: its last argument must be `]`. `type -t [` reports `builtin`, and most systems even ship a standalone `/bin/[` for shells that lack it. So this line: ```bash if [ $answer = yes ]; then ``` means "run the command `[` with the arguments `$answer`, `=`, `yes`, `]`", exactly as if it were `grep`. And like every other command, its arguments are produced by the shell's expansion pipeline first: the variable is substituted, the result is word-split on `IFS`, and the resulting words are glob-expanded. `test` never sees your source line. It sees a list of strings, and it decides what to do purely from **how many** strings there are and what they look like. ## How the two error messages are produced `test` dispatches on argument count. With three arguments it expects `operand OPERATOR operand`. With two, it expects `UNARY-OPERATOR operand`. With `answer=''`, the expansion `$answer` produces **no word at all** — an empty, unquoted expansion vanishes. The call becomes `[ = yes ]`: two arguments before the `]`. Test tries to read `=` as a unary operator and reports `[: =: unary operator expected`. With `answer='I guess'`, the expansion splits into two words. The call becomes `[ I guess = yes ]`: four arguments, no valid shape, so `[: too many arguments`. A third variant bites less often but is nastier. Glob expansion also applies, so `[ $x = *.txt ]` with a single `.txt` file in the current directory expands `*.txt` to that file's name and can report **true** by pure coincidence — and with two matching files it fails with `too many arguments` instead. The condition's answer depends on the working directory. ## Why the script kept going Those failures exit with status **2**, which means "usage error", not "the condition was false" (status 1). But `if` only asks "is the status zero?", so a malformed test is indistinguishable from a false one at the branch. The error text goes to stderr — where, in a cron job or a CI step, nobody reads it — and the script takes the else path. That is the real production risk: not a crash, a silently wrong decision. ## The fix inside `[ ]`: quote ```bash [ "$answer" = yes ] ``` A double-quoted expansion is never split and never globbed, so the call is always three arguments — even when the value is empty, contains spaces, or looks like an operator. Quoting is not a style preference here; it is what makes the argument count deterministic. Braces do **not** help: `[ -f ${f} ]` splits exactly like `[ -f $f ]`. Only quotes do. The defensive idiom you may still see in old scripts, `[ "x$answer" = "xyes" ]`, exists because some ancient `test` implementations mis-parsed a leading `-` operand. In bash it is unnecessary noise. ## What `[[ ]]` changes `[[` is a **reserved word**, like `if` or `while` — `type -t [[` reports `keyword`. The shell parses the whole construct and evaluates the operands internally, which changes the rules: - Expansions inside are **not word-split** and **not pathname-expanded**. `[[ $answer = yes ]]` and `[[ -f $file ]]` are correct even when the value is empty or contains spaces. - It adds pattern matching with `==`/`!=` and regex matching with `=~`. - It supports `&&`, `||` and parentheses **inside** the brackets, so `[[ -f $f && -r $f ]]` is one command. - Because the shell parses it, `<` and `>` are string comparison operators rather than redirections. ```bash answer="I guess" [ $answer = yes ] # [: too many arguments (status 2) [ "$answer" = yes ] # status 1 — a real false [[ $answer = yes ]] # status 1 — a real false, no quotes needed ``` ## What `[[ ]]` does not fix It tames the operands, not your intent. The right-hand side of `==` and `=~` is a *pattern* while it is unquoted, so `[[ $a == $b ]]` is not plain equality. `-eq` and friends still run arithmetic evaluation on their operands. And the moment you leave the brackets — a command argument, an assignment, a `[ ]` in a `#!/bin/sh` script — the ordinary splitting rules are back. The habit to build is "quote by default, and know exactly when you are choosing not to". ## Which to use In a script whose shebang is bash, use `[[ ]]`: it is harder to get wrong and strictly more capable. Reach for `[ ]` when the script must run under a plain POSIX shell, where `[[` is not available at all. Either way, quote your expansions.

  • Is `[` the same thing as `test`?
    Yes — the same builtin under two names, with one difference: `[` insists that its final argument be `]`, while `test` takes none. `test -f "$p"` and `[ -f "$p" ]` are identical calls. Both also exist as external programs (`/bin/test`, `/bin/[`) for shells without the builtin, which is the clearest proof that `[` is a command rather than shell syntax.
  • The failing test returned 2 rather than 1. Does that distinction matter?
    Yes. 0 is true, 1 is a genuine false, and 2 means the test itself was malformed. `if` collapses 1 and 2 into "not true", so a broken condition silently takes the else branch instead of failing. That is why the stderr message matters, and why a job that captures only stdout can run the wrong branch for months undetected.
  • If you quote correctly, is there any reason left to prefer `[[ ]]` in a bash script?
    Several. It gives you pattern matching with `==`, regex matching with `=~` and `BASH_REMATCH`, `&&`/`||` inside a single condition, and `<`/`>` as string comparisons instead of redirections. It also removes a class of future bugs when someone edits the line and forgets the quotes. `[ ]` remains the answer only when the script must run under POSIX sh.

saying these in an interview costs you the question

  • Says [ is bash syntax, like if or then
  • Claims quoting is optional because it works locally
  • Reads the error as "the variable is undefined"
  • Thinks [[ ]] is just a longer spelling of [ ]
  • Assumes a broken test returns 1, so the branch is merely false
  • Believes ${var} braces prevent word splitting

context