What is the difference between [[ $a == z* ]] and [ $a == z* ]?

The difference between [[ … ]] and [ … ] is mostly covered in Why does parameter expansion with spaces without quotes work inside double brackets "[[" but not inside single brackets "["?. Crucially, [[ … ]] is special syntax, whereas [ is a funny-looking name for a command. [[ … ]] has special syntax rules for what's inside, [ … ] doesn't.

With the added wrinkle of a wildcard, here's how [[ $a == z* ]] is evaluated:

  1. Parse the command: this is the [[ … ]] conditional construct around the conditional expression $a == z*.
  2. Parse the conditional expression: this is the == binary operator, with the operands $a and z*.
  3. Expand the first operand into the value of the variable a.
  4. Evaluate the == operator: test if the value of the variable a matches the pattern z*.
  5. Evaluate the conditional expression: its result is the result of the conditional operator.
  6. The command is now evaluated, its status is 0 if the conditional expression was true and 1 if it was false.

Here's how [ $a == z* ] is evaluated:

  1. Parse the command: this is the [ command with the arguments formed by evaluating the words $a, ==, z*, ].
  2. Expand $a into the value of the variable a.
  3. Perform word splitting and filename generation on the parameters of the command.
    • For example, if the value of a is the 6-character string foo b* (obtained by e.g. a='foo b*') and the list of files in the current directory is (bar, baz, qux, zim, zum), then the result of the expansion is the following list of words: [, foo, bar, baz, ==, zim, zum, ].
  4. Run the command [ with the parameters obtained in the previous step.
    • With the example values above, the [ command complains of a syntax error and returns the status 2.

Note: In [[ $a == z* ]], at step 3, the value of a does not undergo word splitting and filename generation, because it's in a context where a single word is expected (the left-hand argument of the conditional operator ==). In most cases, if a single word makes sense at that position then variable expansion behaves like it does in double quotes. However, there's an exception to that rule: in [[ abc == $a ]], if the value of a contains wildcards, then abc is matched against the wildcard pattern. For example, if the value of a is a* then [[ abc == $a ]] is true (because the wildcard * coming from the unquoted expansion of $a matches bc) whereas [[ abc == "$a" ]] is false (because the ordinary character * coming from the quoted expansion of $a does not match bc). Inside [[ … ]], double quotes do not make a difference, except on the right-hand side of the string matching operators (=, ==, != and =~).


[ is an alias for the test command. Unix Version 6 had an if command, but Version 7 (1979) came with the new Bourne shell that had a few programming constructs including the if-then-else-elif-fi construct, and Unix version 7 added a test command that performed most of the "tests" that were carried out by the if command in older versions.

[ was made an alias to test and both were made built into the shell in Unix System III (1981). Though it should be noted that some Unix variants did not have a [ command until much later after that (up to the early 2000s on some BSDs where sh is based on the Almquist shell (a test builtin has always been included in ash's source, but on those BSDs it was initially disabled)).

Note that test aka [ is a command to do "tests", there's no assignment that that command does, so there's no reason to disambiguate between an assignment and equality operator, so the equality operator is =. == is only supported by a few recent implementations of [ (and is just an alias for =).

Because [ is nothing more than a command, it is parsed the same way as any other command by the shell.

Specifically, in your example, $a, because it is not quoted, would be split into several words according to the usual rules of word splitting, and each word would undergo filename generation aka globbing to result into possibly more words, each of those words resulting in a separate argument to the [ command.

Similarly, z* would be expanded to the list of filenames in the current directory starting with z.

So for instance, if $a is b* = x, and there are z1, z2, b1 and b2 files in the current directory, The [ command would get 9 arguments: [, b1, b2, =, x, ==, z1, z2 and ].

[ parses its arguments as a conditional expression. Those 9 arguments don't add up to a valid conditional expression, so it would probably return an error.

The [[ ... ]] construct was introduced by the Korn shell probably around 1988 as ksh86a in 1987 didn't have it while ksh88 had it from the start.

Beside ksh (all implementations), [[...]] is also supported by bash (since version 2.02) and zsh, but all three implementations are different and there are differences between each version of a same shell though the changes are generally backward compatible (a notable exception being bash's =~ operator that has been known to break a few scripts after a certain version when its behavior changed). [[...]] is not specified by POSIX, or Unix or Linux (LSB). It has been considered for inclusion a few times, but not included as the common functionality of it supported by the major shells is already covered by the [ command and the case-in-esac construct.

The whole [[ ... ]] construct makes up a command. That is, it has an exit status (which is its most important asset as it is the result of evaluating the conditional expression), you may pipe it to another command (though it would not be useful), and generally use it wherever you would use any other command (inside the shell only, as it's a shell construct) but it is not parsed like a normal simple command. What's inside is parsed by the shell as a conditional expression and the usual rules of word splitting and filename generation apply differently.

[[ ... ]] does know about == from the start and is equivalent to =1. A blunder of ksh's though (and is causing confusion and many bugs) is that the = and == are not an equality operator but a pattern matching operator (though the matching aspect can be disabled with quoting but with unclear rules that differ from shell to shell).

In your code above [[ $a == z* ]], the shell would parse that into several tokens in rules similar to the usual ones, recognize it as a pattern matching comparison, treat z* as a pattern to match against the content of the a variable.

Generally, it's harder to shoot yourself in the foot with [[ ... ]] than it is with the [ command. But a few rules like

  • always quote variables
  • never use the -a or -o operator (use several [ commands and the && and || shell operators)

Make [ reliable with POSIX shells.

[[...]] in different shells support extra operators like -nt, regexp matching operators... but the list and behavior varies from shell to shell and version to version.

So unless you know what shell and minimum version of it your script will ever be interpreted by, it's probably safer to stick with the standard [ command.


1An exception: [[...]] was added to bash in version 2.02. Until 2.03 where it was changed, [[ x = '?' ]] would return true while [[ x == '?' ]] would return false. That is quoting did not prevent pattern matching when using the = operator in those versions, but did when using ==.