Home » Why is `[` a shell builtin and `[[` a shell keyword?

Why is `[` a shell builtin and `[[` a shell keyword?


The difference between [ and [[ 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 $name and 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 $name but, 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:

    $ 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.

Because [[ 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.

[ and test may be implemented using the same code. This is the case for /bin/[ and /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 /bin/test.⁴

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.⁵


  1. 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 iftest"$x"...

  2. It happened around 1980. /bin/[ doesn’t exist in my copy of Ancient Unix V7 from 1979, nor does man test document 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.

  3. ls -i /bin/[ /bin/test

  4. But don’t count on this behavior. The Bash built-in version of [ does require the closing ], and its built-in test implementation will complain if you do provide it.

  5. The builtin vs external command distinction may also matter for another reason: the two implementations may behave differently. This is the case for echo on 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 [ and 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 > and < 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, < and > 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 [ command/builtin.

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 expr command.

Related Solutions

Joining bash arguments into single string with spaces

[*] I believe that this does what you want. It will put all the arguments in one string, separated by spaces, with single quotes around all: str="'$*'" $* produces all the scripts arguments separated by the first character of $IFS which, by default, is a space....

AddTransient, AddScoped and AddSingleton Services Differences

TL;DR Transient objects are always different; a new instance is provided to every controller and every service. Scoped objects are the same within a request, but different across different requests. Singleton objects are the same for every object and every...

How to download package not install it with apt-get command?

Use --download-only: sudo apt-get install --download-only pppoe This will download pppoe and any dependencies you need, and place them in /var/cache/apt/archives. That way a subsequent apt-get install pppoe will be able to complete without any extra downloads....

What defines the maximum size for a command single argument?

Answers Definitely not a bug. The parameter which defines the maximum size for one argument is MAX_ARG_STRLEN. There is no documentation for this parameter other than the comments in binfmts.h: /* * These are the maximum length and maximum number of strings...

Bulk rename, change prefix

I'd say the simplest it to just use the rename command which is common on many Linux distributions. There are two common versions of this command so check its man page to find which one you have: ## rename from Perl (common in Debian systems -- Ubuntu, Mint,...

Output from ls has newlines but displays on a single line. Why?

When you pipe the output, ls acts differently. This fact is hidden away in the info documentation: If standard output is a terminal, the output is in columns (sorted vertically) and control characters are output as question marks; otherwise, the output is...

mv: Move file only if destination does not exist

mv -vn file1 file2. This command will do what you want. You can skip -v if you want. -v makes it verbose - mv will tell you that it moved file if it moves it(useful, since there is possibility that file will not be moved) -n moves only if file2 does not exist....

Is it possible to store and query JSON in SQLite?

SQLite 3.9 introduced a new extension (JSON1) that allows you to easily work with JSON data . Also, it introduced support for indexes on expressions, which (in my understanding) should allow you to define indexes on your JSON data as well. PostgreSQL has some...

Combining tail && journalctl

You could use: journalctl -u service-name -f -f, --follow Show only the most recent journal entries, and continuously print new entries as they are appended to the journal. Here I've added "service-name" to distinguish this answer from others; you substitute...

how can shellshock be exploited over SSH?

One example where this can be exploited is on servers with an authorized_keys forced command. When adding an entry to ~/.ssh/authorized_keys, you can prefix the line with command="foo" to force foo to be run any time that ssh public key is used. With this...

Why doesn’t the tilde (~) expand inside double quotes?

The reason, because inside double quotes, tilde ~ has no special meaning, it's treated as literal. POSIX defines Double-Quotes as: Enclosing characters in double-quotes ( "" ) shall preserve the literal value of all characters within the double-quotes, with the...

What is GNU Info for?

GNU Info was designed to offer documentation that was comprehensive, hyperlinked, and possible to output to multiple formats. Man pages were available, and they were great at providing printed output. However, they were designed such that each man page had a...

Set systemd service to execute after fstab mount

a CIFS network location is mounted via /etc/fstab to /mnt/ on boot-up. No, it is not. Get this right, and the rest falls into place naturally. The mount is handled by a (generated) systemd mount unit that will be named something like mnt-wibble.mount. You can...

Merge two video clips into one, placing them next to each other

To be honest, using the accepted answer resulted in a lot of dropped frames for me. However, using the hstack filter_complex produced perfectly fluid output: ffmpeg -i left.mp4 -i right.mp4 -filter_complex hstack output.mp4 ffmpeg -i input1.mp4 -i input2.mp4...

How portable are /dev/stdin, /dev/stdout and /dev/stderr?

It's been available on Linux back into its prehistory. It is not POSIX, although many actual shells (including AT&T ksh and bash) will simulate it if it's not present in the OS; note that this simulation only works at the shell level (i.e. redirection or...

How can I increase the number of inodes in an ext4 filesystem?

It seems that you have a lot more files than normal expectation. I don't know whether there is a solution to change the inode table size dynamically. I'm afraid that you need to back-up your data, and create new filesystem, and restore your data. To create new...

Why doesn’t cp have a progress bar like wget?

The tradition in unix tools is to display messages only if something goes wrong. I think this is both for design and practical reasons. The design is intended to make it obvious when something goes wrong: you get an error message, and it's not drowned in...