https://jmmv.dev/2020/03/test-bracket.html
# jmmv.dev
* About
* Blog
* Resume
* Software
* Archive
* Series
* Tags
[ ]
[search]
test, [, and [[
March 25, 2020 * About 5 minutes * Tags: shell
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/[
Uh huh. [? The square bracket? That's a program?!
But wait, it gets more interesting:
$ ls -li /bin/[ /bin/test
834 -rwxr-xr-x 2 root wheel 35824 Jan 23 08:59 /bin/[
834 -rwxr-xr-x 2 root wheel 35824 Jan 23 08:59 /bin/test
The two names, [ and test, point to the same binary^1. But why? What
are these?
The 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[0] 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 true and 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,
test and [ 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 echo.)
---------------------------------------------------------------------
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* against 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 !, &&, and || 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.
---------------------------------------------------------------------
1. /bin/[ and /bin/test are not necessarily hard links. They indeed
are on NetBSD and I had assumed that was true on all systems&
mldr; 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. -[?]
<< Previous
All posts
Next >>
(0)
(0)
Share on Reddit
Share on Hacker News
Share on Twitter
[pen]
[ ]
Your name (optional): [ ]
Your email (optional): [ ] Invisible to all
readers; if provided, you will receive notifications when replied to
(not implemented yet)
Post Comments are subject to moderation. This feature is experimental
and is powered by EndTRACKER. If you experience any issues, please
contact me off-band.
[20181124-s]
Julio Merino
A blog on operating systems, programming languages, testing, build
systems, my own software projects and even personal productivity.
Specifics include FreeBSD, Linux, Rust, Bazel and EndBASIC.
[ ] Subscribe
0 subscribers
Follow @jmmv on Mastodon Follow @jmmv on Twitter RSS feed
Featured software
* EndBASIC: Online BASIC+DOS env
* EndTRACKER: Services for static sites
Featured posts
* Fast machines, slow machines
* EndBASIC 0.10: Core language, evolved
* Farewell, Microsoft; hello, Snowflake!
* Rust is hard, yes, but does it matter?
* Rust traits and dependency injection
* A year on Windows: Introduction
* Always be quitting
* How does Google keep build times low?
* How does Google avoid clean builds?
* Unit-testing a console app (a text editor)
* More...
Back to top
Copyright 2004-2023 Julio Merino
[stamp]