Linux & Bash · Part I · Module 16 · Conditionals and Pattern Branches
test, [ and [[: Three Spellings, Different Rules
The square brackets in a Bash conditional are the most misread characters in the language. They are not syntax, not punctuation and not part of if — the opening bracket is the name of a command, and there is a real executable called that on your disk. This chapter proves it and then shows what the double bracket does differently.
Intermediate22 min readLinuxPart IBashConditionalstestQuotingScripting
Part I · Module 16 · Chapter 16.2 · test, [ and [[: Three Spellings, Different Rules
Chapter 16.1 used [ -z "$log" ] and asked you to accept it. Here is the payment on that debt, and it starts with something you can check in two seconds.
1. There Is a File Called /bin/[
$ type -a [
[ is a shell builtin
[ is /bin/[
$ ls -l '/bin/['
-rwxr-xr-x 1 root wheel 101456 Jun 25 07:59 /bin/[(The quotes are for your interactive shell, not for [. An unquoted /bin/[ is a half-finished character-class glob — Chapter 3.5 — and zsh in particular refuses it outright with bad pattern.)
A hundred-kilobyte executable whose filename is a single open square bracket. It is not a trick of the shell, not an alias, not a symlink to something more sensible. It is a program, it has been shipped on Unix systems since the 1970s, and you can run it:
$ /bin/[ -f /etc/hosts ]; echo $?
0
$ /bin/[ -f /nope ]; echo $?
1The same is true of its older, more honest spelling:
$ type -a test
test is a shell builtin
test is /bin/test
$ test -f /etc/hosts; echo $?
0test and [ are the same program, shipped under two names. On this machine they are even two separate files — 101456 and 101472 bytes, different inodes, sixteen bytes apart because one of them carries a longer name and an extra argument check. Modern Bash has both built in so it does not have to launch a process for every conditional, but the standalone executables remain, and the builtins behave the way the files do. You met which -a and the builtin-before-file lookup order in Chapter 10.2; this is that rule applied to the strangest filename on your system.
2. Therefore the Spaces Are Not Optional
If [ is a command name, then [-f is a different command name. Bash will go looking for it:
$ [-f /etc/hosts]
bash: [-f: command not foundRead that error message carefully, because it is the proof. Not "syntax error". Not "invalid condition". command not found — the same message you get from a typo in any command name, because that is exactly what happened. Bash split the line into words, took the first word [-f, searched PATH for something called that, and found nothing.
Now the other end:
$ [ -f /etc/hosts]
bash: [: missing `]'A different error, from a different source. This time Bash found the command [ and ran it with two arguments, -f and /etc/hosts]. The command itself then complained: it requires its last argument to be exactly ], and it got something else. The message is ['s own, which is why it is prefixed with the program's name.
3. Therefore the Contents Are Arguments
This is where it stops being a curiosity and starts costing people hours. If what is inside [ ] is an argument list, then everything Module 9 said about expansion applies, unchanged.
$ v=""
$ [ $v = "" ]; echo $?
bash: [: =: unary operator expected
2An error — from a test that looks completely reasonable. Here is why. $v is empty and unquoted, so Chapter 9.2's word-splitting removes it entirely. The argument list [ actually received was:
[ = "" ]Three arguments before the bracket, with = first. [ reads = in the position where it expects a value, tries to interpret it as a one-argument test, and gives up. Quote the variable and the word survives:
$ [ "$v" = "" ]; echo $?
0Now the argument list is [, "", =, "", ] — four arguments, the shape [ expects, and the test runs.
The other half of the same problem:
$ name="my report"
$ [ $name = "my report" ]; echo $?
bash: [: too many arguments
2Unquoted, my report split into two words, so [ got five arguments where it wanted three and refused. Again, quoting fixes it:
$ [ "$name" = "my report" ]; echo $?
0Inside [ ], quote every variable. Every single one. Not as a style preference — as the only way the test is correct for all possible values. You cannot predict what a variable holds, and the failure mode is not a wrong answer but a thrown error that may well be swallowed by a redirect.
4. The Double Bracket Is Not a Command
Now compare:
$ type [[
[[ is a shell keywordA keyword, like if and then — not a builtin, not a file. There is no /bin/[[, and /bin/[[ could not work even if someone made one, because [[ changes how Bash parses, and a program cannot do that from outside.
The practical consequence is everything:
$ v=""
$ [[ $v = "" ]]; echo $?
0
$ name="my report"
$ [[ $name = "my report" ]]; echo $?
0Both of them, unquoted, with no error. Bash saw [[ while parsing, recognised it as the start of a conditional construct, and handled the contents itself. No argument list was ever built, so there was nothing for word-splitting to split.
A command versus a piece of syntax
5. What the Double Bracket Adds
Because Bash is parsing, it can offer things a command never could.
Glob patterns on the right-hand side. This is the big one:
$ n="run_42.log"
$ [[ $n == run_* ]] && echo "matched"
matched
$ [ "$n" = "run_*" ] && echo "matched"
$Inside [[ ]], the unquoted right-hand side of == is a pattern, matched with the same glob rules as a filename in Chapter 3.5. [ ] has no such concept: = compares strings literally, so it asked whether the filename was the eight characters run_* and correctly said no.
Those are the same glob rules, so all of Chapter 3.5's syntax works:
$ f="top.sv"
$ [[ $f == *.sv ]] && echo "systemverilog"
systemverilog
$ [[ $f == [a-z]* ]] && echo "starts lowercase"
starts lowercaseThe extended patterns need extglob, and they need it switched on before the line is parsed — in a script, not on the same command line:
#!/usr/bin/env bash
shopt -s extglob
f="top.sv"
[[ $f == *.@(sv|v) ]] && echo "an HDL file" # prints: an HDL filePut shopt -s extglob and the test on one interactive line and Bash reports syntax error near before the shopt has run. That is Chapter 10.6's shell options meeting Chapter 9.1's expansion order, and the fix is simply to set the option somewhere earlier than the line that needs it.
Regular expressions, with =~:
$ n="run_42.log"
$ [[ $n =~ ^run_[0-9]+\.log$ ]] && echo "well-formed name"
well-formed name
$ bad="run_x.log"
$ [[ $bad =~ ^run_[0-9]+\.log$ ]] || echo "rejected"
rejectedThat is the ERE syntax from Chapter 14.3, evaluated by the shell with no external command. It is genuinely more than [ ] can do at any length — a glob cannot say "one or more digits".
Operators that do not collide with the shell. &&, || and parentheses work inside [[ ]] as logical operators rather than as shell control:
$ [[ -f /etc/hosts && -r /etc/hosts ]] && echo "exists and readable"
exists and readableChapter 16.5 is the full treatment of compound conditions and why this form is preferred.
6. The Redirection Trap
The single most dangerous difference, and the strongest argument for [[ ]]:
$ ls
$ [ 10 > 9 ] && echo "ten is greater"
ten is greater
$ ls
9Two things just went wrong. There is now a file called 9 in your directory, and the test reported true no matter what the numbers were.
[ is a command, so > between its arguments is a redirection — Module 12's output redirection, behaving exactly as specified. Bash removed > 9 from the argument list, created the file 9, and ran [ 10 ], which is a one-argument non-empty-string test on 10, which is true. The comparison never happened.
$ [ 1 > 999999 ] && echo "one is greater than a million"
one is greater than a millionIt will report true for any pair of numbers, forever, and quietly litter the directory with files named after the right operand.
In [ ] the operator has to be escaped from the shell, and even then it is a string comparison:
$ [ 10 \> 9 ] && echo "greater" || echo "not greater"
not greaterFalse — because as strings, 1 sorts before 9. Chapter 16.3 is about exactly that mismatch.
And in [[ ]], none of this arises:
$ [[ 10 > 9 ]] && echo "greater" || echo "not greater"
not greaterNo file created, no escaping needed — Bash is parsing, so > is an operator here, not a redirection. (Still a string comparison, still false; still Chapter 16.3's problem. But it did not silently write to your disk.)
7. So Which One
[ ] / test | [[ ]] | |
|---|---|---|
| What it is | a command | shell syntax |
| Available in | every POSIX shell | bash, ksh, zsh |
| Unquoted empty variable | error | fine |
| Unquoted value with spaces | error | fine |
| Glob pattern match | no | yes, with == |
| Regex match | no | yes, with =~ |
&& and || inside | no | yes |
> between operands | silently redirects | compares |
Use [[ ]] in anything with a Bash shebang. It is safer by default, it is more capable, and every one of its differences from [ ] is a difference in your favour. Chapter 15.2 had you write #!/usr/bin/env bash — this is one of the things that buys.
Use [ ] when the script must run on a shell that is not Bash — a #!/bin/sh script, something inside a Makefile recipe, a container built on BusyBox or Alpine, a dash login script. There [[ is a syntax error, not a fallback:
$ dash -c '[[ -f /etc/hosts ]] && echo yes'
dash: 1: [[: not foundKnow [ ] regardless, because you will read it constantly. Decades of shell scripts, every autoconf-generated configure, and most of /etc use it, and you cannot maintain code you cannot parse.
8. One Honest Caveat About [[ ]]
There is one place where [[ ]]'s cleverness is a trap:
$ [ abc -eq 1 ]; echo $?
bash: [: abc: integer expression expected
2
$ [[ abc -eq 1 ]]; echo $?
1[ ] told you that abc is not a number. [[ ]] silently returned false. Inside [[ ]], -eq evaluates its operands in an arithmetic context, where a non-numeric string becomes 0 — so the test quietly became 0 -eq 1, which is false, with no complaint.
For a script reading a line out of a log file, that is the difference between a clear error and a wrong answer that looks fine. It is not a reason to avoid [[ ]]; it is a reason to validate input you do not control before comparing it, which Chapter 16.3 shows how to do.
Practice
- Run
ls -l '/bin/[' /bin/testand compare the two files — size, permissions, inode (ls -i). Are they the same file? What doestype -asay about each name? - Run
[-d /tmp], then[ -d /tmp], then[ -d /tmp ]. Write down all three outcomes and explain each error message by naming which program produced it. - In an empty directory, run
[ 5 > 3 ] && echo yes, thenls. Explain both results. Now do the same with[[ 5 > 3 ]]and compare. - Set
v=""and run all four of[ $v = "" ],[ "$v" = "" ],[[ $v = "" ]],[[ "$v" = "" ]]. Three succeed. Explain the one that does not in terms of the argument list[received. - Set
f="top_tb.sv". Write a[[ ]]test that is true for any.svor.vfile, then the closest[ ]equivalent you can manage. What did it cost you? - Run
dash -c '[[ -f /etc/hosts ]]'anddash -c '[ -f /etc/hosts ]'. Which fails, and what does that tell you about a#!/bin/shscript? - Set
n="42x"and compare[ "$n" -gt 10 ]with[[ $n -gt 10 ]]. Which behaviour would you rather have in a script parsing a log, and why?
Check Your Understanding
- Why is
[-f file]reported ascommand not found? - Why must there be a space before the closing
]? vis empty. Why does[ $v = "" ]error while[[ $v = "" ]]does not?- What does
[ 10 > 9 ]do? - What is the one thing
[[ ]]cannot give you that[ ]can? [[ $f == *.sv ]]is true fortop.sv. Why is[[ $f == "*.sv" ]]false?
9. Remember
[is a command. There is a real executable at/bin/[, andtestis the same program under its original name.- The spaces are mandatory because
[is a command name and]is an argument.[-f x]iscommand not found;[ -f x]ismissing ']'. - Inside
[ ], quote every variable. An unquoted empty value vanishes and an unquoted spaced value splits, and both produce errors rather than wrong answers. [[ ]]is a shell keyword, so Bash parses its contents and nothing inside is ever split. Quoting becomes optional.[[ ]]adds glob matching with==, regex with=~, and&&/||/ parentheses as logical operators.- Quoting the right side of
==inside[[ ]]turns the pattern off and makes it a literal comparison. [ 10 > 9 ]creates a file called9and reports true.[[ 10 > 9 ]]does not.- Prefer
[[ ]]under abashshebang; use[ ]under#!/bin/sh. Read both fluently either way. [[ ]]hides a non-numeric operand where[ ]reports it. Validate values you did not produce.
Chapter 16.3 takes the comparison itself. Both brackets will happily compare 10 and 9 and tell you that 10 is smaller — and the reason is not a bug but a choice of operator.
Where this fits
Part of the Linux & Bash curriculum.
