Skip to content
VLSI Mentor

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/[

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ /bin/[ -f /etc/hosts ]; echo $?
0
$ /bin/[ -f /nope ]; echo $?
1

The same is true of its older, more honest spelling:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ type -a test
test is a shell builtin
test is /bin/test

$ test -f /etc/hosts; echo $?
0

test 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [-f /etc/hosts]
bash: [-f: command not found

Read 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ -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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ v=""
$ [ $v = "" ]; echo $?
bash: [: =: unary operator expected
2

An 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[   =   ""   ]

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ "$v" = "" ]; echo $?
0

Now the argument list is [, "", =, "", ] — four arguments, the shape [ expects, and the test runs.

The other half of the same problem:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ name="my report"
$ [ $name = "my report" ]; echo $?
bash: [: too many arguments
2

Unquoted, my report split into two words, so [ got five arguments where it wanted three and refused. Again, quoting fixes it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ "$name" = "my report" ]; echo $?
0

Inside [ ], 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ type [[
[[ is a shell keyword

A 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ v=""
$ [[ $v = "" ]]; echo $?
0

$ name="my report"
$ [[ $name = "my report" ]]; echo $?
0

Both 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

The difference between the single bracket and the double bracket. The single bracket is an ordinary command whose name happens to be a bracket, so its contents are arguments and every expansion inside it is split and matched like any other. The double bracket is shell syntax, so the shell parses the contents itself and nothing inside is split.A single bracket is acommandits name really is thatcharacterSo its contents areargumentswhich is why the spaces aremandatoryAnd expansions aresplitan unquoted empty valuebecomes no argument at allA double bracket issyntaxthe shell parses it, not aprogramSo the shell sees thewhole thingbefore any splitting couldhappenAnd nothing inside issplitquoting becomes optional, andpatterns work12
The single bracket is an ordinary command, so its contents become an argument list and every expansion rule from Module 9 applies to them. The double bracket is parsed by the shell itself, so no argument list is built and nothing inside it can be split. That one difference generates every practical difference between the two.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ f="top.sv"
$ [[ $f == *.sv ]]        && echo "systemverilog"
systemverilog
$ [[ $f == [a-z]* ]]      && echo "starts lowercase"
starts lowercase

The extended patterns need extglob, and they need it switched on before the line is parsed — in a script, not on the same command line:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/usr/bin/env bash
shopt -s extglob
f="top.sv"
[[ $f == *.@(sv|v) ]] && echo "an HDL file"      # prints: an HDL file

Put 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 =~:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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"
rejected

That 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [[ -f /etc/hosts && -r /etc/hosts ]] && echo "exists and readable"
exists and readable

Chapter 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 [[ ]]:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ls
$ [ 10 > 9 ] && echo "ten is greater"
ten is greater
$ ls
9

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ 1 > 999999 ] && echo "one is greater than a million"
one is greater than a million

It 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ 10 \> 9 ]  && echo "greater" || echo "not greater"
not greater

False — because as strings, 1 sorts before 9. Chapter 16.3 is about exactly that mismatch.

And in [[ ]], none of this arises:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [[ 10 > 9 ]] && echo "greater" || echo "not greater"
not greater

No 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 isa commandshell syntax
Available inevery POSIX shellbash, ksh, zsh
Unquoted empty variableerrorfine
Unquoted value with spaceserrorfine
Glob pattern matchnoyes, with ==
Regex matchnoyes, with =~
&& and || insidenoyes
> between operandssilently redirectscompares

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ dash -c '[[ -f /etc/hosts ]] && echo yes'
dash: 1: [[: not found

Know [ ] 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ 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

  1. Run ls -l '/bin/[' /bin/test and compare the two files — size, permissions, inode (ls -i). Are they the same file? What does type -a say about each name?
  2. 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.
  3. In an empty directory, run [ 5 > 3 ] && echo yes, then ls. Explain both results. Now do the same with [[ 5 > 3 ]] and compare.
  4. 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.
  5. Set f="top_tb.sv". Write a [[ ]] test that is true for any .sv or .v file, then the closest [ ] equivalent you can manage. What did it cost you?
  6. Run dash -c '[[ -f /etc/hosts ]]' and dash -c '[ -f /etc/hosts ]'. Which fails, and what does that tell you about a #!/bin/sh script?
  7. 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

  1. Why is [-f file] reported as command not found?
  2. Why must there be a space before the closing ]?
  3. v is empty. Why does [ $v = "" ] error while [[ $v = "" ]] does not?
  4. What does [ 10 > 9 ] do?
  5. What is the one thing [[ ]] cannot give you that [ ] can?
  6. [[ $f == *.sv ]] is true for top.sv. Why is [[ $f == "*.sv" ]] false?

9. Remember

  • [ is a command. There is a real executable at /bin/[, and test is the same program under its original name.
  • The spaces are mandatory because [ is a command name and ] is an argument. [-f x] is command not found; [ -f x] is missing ']'.
  • 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 called 9 and reports true. [[ 10 > 9 ]] does not.
  • Prefer [[ ]] under a bash shebang; 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.