The difference between
[[ is quite fundamental.
[is a command. Its arguments are processed just the way any other commands arguments are processed. For example, consider:
[ -z $name ]
The shell will expand
$nameand perform both word splitting and filename generation on the result, just as it would for any other command.
As an example, the following will fail:
$ name="here and there" $ [ -n $name ] && echo not empty bash: [: too many arguments
To have this work correctly, quotes are necessary:
$ [ -n "$name" ] && echo not empty not empty
[[is a shell keyword and its arguments are processed according to special rules. For example, consider:
[[ -z $name ]]
The shell will expand
$namebut, unlike any other command, it will perform neither word splitting nor filename generation on the result. For example, the following will succeed despite the spaces embedded in
$ name="here and there" $ [[ -n $name ]] && echo not empty not empty
[ is a command and is subject to the same rules as all other commands that the shell executes.
[[ is a keyword, not a command, however, the shell treats it specially and it operates under very different rules.
In V7 Unix — where the Bourne shell made its debut —
[ was called
test, and it existed only as
/bin/test. So, code you would write today as:
if [ "$foo" = "bar" ] ; then ...
you would have written instead as
if test "$foo" = "bar" ; then ...
This second notation still exists, and I find that it’s more clear about what’s going on: you are calling a command called
test, which evaluates its arguments and returns an exit status code that
if uses to decide what to do next. That command may be built into the shell, or it may be an external program.¹
[ as an alternative to
test came later.² It may be a builtin synonym for
test, but it is also provided as
/bin/[ on modern systems for shells that do not have it as a builtin.
test may be implemented using the same code. This is the case for
/bin/test on OS X, where these are hard links to the same executable.³ As a result, the implementation completely ignores the trailing
]: it doesn’t require it if you call it as
/bin/[, and it doesn’t complain if you do provide it to
None of that history affects
[[, because there never was a primordial program called
[[. It exists purely inside those shells that implement it as an extension to the POSIX shell.
Part of the distinction between “builtin” and “keyword” is due to this history. It also reflects the fact that the syntax rules for parsing
[[ expressions is different, as pointed out in John1024’s answer.⁵
When you look at it that way, it makes it clear why you must put spaces around
[in shell scripts, unlike the way parentheses and brackets work in most other programming languages. If the shell’s command parser allowed
if["$x"..., it would also have to allow
It happened around 1980.
/bin/[doesn’t exist in my copy of Ancient Unix V7 from 1979, nor does
man testdocument it as an alias. In the corresponding man page entry I have in a pre-release copy of the System III manual from 1980, it is listed.
ls -i /bin/[ /bin/test
But don’t count on this behavior. The Bash built-in version of
[does require the closing
], and its built-in
testimplementation will complain if you do provide it.
The builtin vs external command distinction may also matter for another reason: the two implementations may behave differently. This is the case for
echoon many systems. Because there is only one implementation, no such distinction needs to be made for a keyword.
[ was originally just an external command, another name for
/bin/test. But a few commands, such as
echo, are used so frequently in shell scripts that the shell implementors decided to copy the code directly into the shell itself, rather than have to run another process every time they’re used. That turned these commands into “builtins”, although you can still invoke the external program via its full path.
[[ came much later. Although the builtin is implemented internally within the shell, it’s parsed just like external commands. As explained in John1024’s answer, this means that unquoted variables will get word splitting done on them, and tokens like
< are processed as I/O redirection. This made writing complex comparison expressions inconvenient.
[[ was created as shell syntax, so that it could be parsed ideosyncratically. Within
[[ variables don’t get word splitting,
> can be used as comparison operators,
= can behave differently depending on whether the next parameter is quoted or not, etc. These are all conveniences that make
[[ easier to use than the traditional
They couldn’t simply recode
[ as syntax like this because it would have been an incompatible change to millions of scripts. By using the new
[[ syntax, which didn’t previously exist, they could totally revamp the way it’s used in an upward compatible way.
This is similar to the evolution that resulted in
$((...)) syntax for arithmetic expressions, which has mostly replaced the traditional