[ 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 are, how they differ, real-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:
/bin/shscripts- BusyBox / Alpine Linux
- Init scripts
- Embedded systems
- Minimal Docker images
Portable Example
#!/bin/sh
if [ -f "/etc/passwd" ]; then
echo "File exists"
fi
📌 Best Practice If your script starts with
#!/bin/sh, use[ ].
When Should You Use [[ test ]]? (Recommended)
Use [[ ]] when:
- Writing Bash or Zsh scripts
- Writing
.bashrcor.zshrc - Writing CI/CD pipelines
- Writing developer tooling
- Safety and maintainability matter
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 [[ ]]:
- Fewer quoting bugs
- No glob surprises
- Cleaner logic
- Better readability
Rule of Thumb (Bookmark This)
❓ Does this script need to run in plain
/bin/sh?
| Answer | Use |
|---|---|
| Yes | [ test ] |
| No | [[ test ]] |
Conclusion
[ test ]exists for portability[[ test ]]exists for safety and power- They are not interchangeable
- Using the wrong one can introduce silent production bugs
If you are writing modern Bash or Zsh scripts, default to [[ ]] and use [ ] only when portability is a strict requirement.