Did you know that Unix systems have a binary whose name is a single symbol?
Go and look for it. Run
ls /bin/? and behold:
$ ls /bin/? /bin/[
[? The square bracket? That’s a program?!
But wait, it gets more interesting:
$ ls -li /bin/[ /bin/test 834 -rwxr-xr-x 1 root wheel 35824 Jan 23 08:59 /bin/[ 834 -rwxr-xr-x 1 root wheel 35824 Jan 23 08:59 /bin/test
The two names,
test, point to the same binary1. But why? What are these?
test program is what you use in the shell to evaluate an expression. You can use this to compare strings, compare numbers, and to check various conditions on files. If you have written a shell script—any shell script really—you certainly have used either of these variants.
The way this works is simple: the
test program takes a bunch of arguments, evaluates the expression represented by them, and returns 0 if the expression is true or 1 if it is false. This then lets you do things like:
if test a = b; then echo "The two strings were the same! Oops!" fi
So why do we have two names for this helper tool? I haven’t been able to find the definitive answer, but my guess is simply: because the above “looks ugly”, and the “obvious” solution to make it look nicer is to introduce
[ as a command. With it, you can express the same logic from above as:
if [ a = b ]; then echo "The two strings were the same! Oops!" fi
Yup. Exact same code as above. The only difference here is that the
test binary checks its
argv to see if it’s invoked as
test or as
[. If the invocation happens to use the latter, then the program ensures that the last argument is the matching
] to keep things balanced.
With that, you can deduce that you don’t even need the conditional statement to use either of these commands and see what’s going on:
$ test a = a; echo $? 0 $ test a = b; echo $? 1 $ [ a = a ]; echo $? 0 $ [ a = b ]; echo $? 1
That’s right. The
if statement we used in the previous examples just takes a command as its argument and runs it to get its exit code. (And with this, you can “guess” that
false are… yup, yup… also helper binaries.)
To make things more confusing, though, pay attention to the following:
$ /bin/test a b test: a: unexpected operator $ test a b dash: 2: test: a: unexpected operator
Why did we get different outputs there? Well… as it so happens,
[ appear a lot in shell scripts. Invoking them as separate binaries would be very inefficient, so the vast majority of the shells implement these commands as built-ins too. You may get different behavior depending on whether you run the external binary or the builtin, which means you easily get different behavior across different shells. (And that’s true for many other things like the innocent-looking
So what about
[[? This is a Bash extension and replaces the use of
[. The key difference, however, is that
[[ is guaranteed to be a builtin and therefore it can change, and it does change, the fundamental rules of the language within the expression it evaluates. To illustrate this, let’s look at an example with globs:
$ touch long-name $ [ long* = long-name ] && echo match match $ [[ long* = long-name ]] && echo match
The first command shown here is an invocation of the
[ tool, which may or may not be a builtin. No matter what, all arguments are subject to the regular shell expansion rules, so
long* is matched against the directory contents, is then expanded to
long-name, and thus the test succeeds. But, in contrast,
[[ produces a different result because it treats the
long* as a literal string, so all this is doing is comparing
long-name verbatim, and therefore failing.
What should you use, then? If you are writing a portable shell script (please do), then stick to
[. You can also use
test, but I don’t think that’s too common. But if you know your script is going to be Bash-specific anyway, you are probably better served by using
[[ unconditionally and consistently, as it provides a lot of nice features (like regular expression matches via
And now for the final lolz. I’ve said above that these are the commands you use to evaluate expressions… but the shell also has expressions of its own via the
|| operators—all of which work on command exit statuses. That is:
$ grep ^hello$ /usr/share/dict/words && grep ^bye$ /usr/share/dict/words hello bye $ echo $? 0 $ grep ^tyop$ /usr/share/dict/words && grep ^bye$ /usr/share/dict/words $ echo $? 1
Which means… that you can combine
test expressions and shell expressions in one invocation:
if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded" fi
You pick whether to be amused or horrified. I don’t know how exactly my coworker reacted when I hinted at this during a recent code review I did for them.
/bin/testare not necessarily hard links. They indeed are on NetBSD and I had assumed that was true on all systems… but a cursory look shows that macOS Catalina ships these as two separate copies of the same binary and Debian testing just has two different binaries. The POSIX spec does not require them to be links after all. ↩︎