Reading Time: 3 minutes

[ test ] vs [[ test ]] in Shell Scripting (Bash & Zsh): Differences, Examples, and Best Practices


Introduction

Shell scripting often appears simple—until a conditional expression silently breaks your script.

One of the most common and costly mistakes in Bash and Zsh scripting is misunderstanding the difference between:

[ test ]

and

[[ test ]]

Although they look similar, they behave very differently. This article explains what they arehow they differreal-world failure cases, and which one you should use in practice.


What Is [ test ] vs [[ test ]]?

Key Difference (High-Impact Concept)

[ test ] is an external command. [[ test ]] is a shell keyword.

That single distinction explains nearly all behavioral differences.

Comparison Table

Feature [ test ] [[ test ]]
Type External command (/usr/bin/[) Shell keyword
POSIX compliant Yes No
Word splitting Yes No
Globbing Yes No
Regex support No Yes
Safer defaults No Yes

Visual Diagram: How the Shell Interprets Them

[ test ]                         [[ test ]]
--------                         ---------
Shell → expands variables        Shell → keeps variables intact
      → word splitting                 → no word splitting
      → glob expansion                 → pattern matching
      → exec /usr/bin/[                → evaluated internally

Example 1: Unquoted Variables (Most Common Bug)

Code Example

var="a b"

[ $var = "a b" ]
echo "[ ] exit code: $?"

[[ $var = "a b" ]]
echo "[[ ]] exit code: $?"

Output

[ ]: too many arguments
[ ] exit code: 2
[[ ]] exit code: 0

Why This Happens

$var → "a b"

[ $var = "a b" ]
↓
[ a b = a b ]   ❌ invalid syntax

[[ ]] does not split words, so it evaluates correctly.

⚠️ Production Bug Many real-world shell bugs are caused by forgetting to quote variables when using [ ].


Example 2: Wildcards and Globbing

file="notes.txt"

[ $file = *.txt ]
[[ $file = *.txt ]]

Behavior Comparison

Construct What Happens
[ ] *.txt expands to files in the directory
[[ ]] *.txt is treated as a pattern

If the directory contains multiple .txt files, [ ] may fail unexpectedly.

💡 Tip [[ ]] prevents accidental glob expansion, one of the hardest shell bugs to debug.


Example 3: Regular Expressions (Only [[ ]] Supports This)

username="user_123"

[[ $username =~ ^user_[0-9]+$ ]] && echo "Valid username"

Attempting this with [ ] results in a syntax error.

[ user_123 =~ ^user_[0-9]+$ ]  ❌

Example 4: Empty or Unset Variables

unset value

[ $value = "x" ]     # Error
[[ $value = "x" ]]   # Safe

Diagram:

[ $value = "x" ]
↓
[ = x ]   ❌

[[ ]] safely handles empty variables without defensive quoting.


When Should You Use [ test ]?

Use [ ] ONLY When Portability Is Required

Valid use cases:

Portable Example

#!/bin/sh

if [ -f "/etc/passwd" ]; then
  echo "File exists"
fi

📌 Best Practice If your script starts with #!/bin/sh, use [ ].


Use [[ ]] when:

Example

if [[ $env == prod* && $version =~ ^v[0-9]+$ ]]; then
  deploy
fi

This is clearer, safer, and more expressive.


Performance and Safety Considerations

[ test ]     → fork + exec → slower
[[ test ]]   → shell built-in → faster

Additional benefits of [[ ]]:


Rule of Thumb (Bookmark This)

❓ Does this script need to run in plain /bin/sh?

Answer Use
Yes [ test ]
No [[ test ]]

Conclusion

If you are writing modern Bash or Zsh scripts, default to [[ ]] and use [ ] only when portability is a strict requirement.