Test Expressions — [ ], [[ ]], and test
The different ways to test conditions in Bash: POSIX test/[ ], bash [[ ]], and arithmetic (( )). Know when to use which and avoid common pitfalls.
Bash offers three ways to evaluate conditions: the POSIX test command (also written [ ]), the Bash-specific [[ ]] (compound command), and arithmetic (( )). Knowing the difference helps you write safer, more readable scripts.
test and [ ] — POSIX Compatible
test expr and [ expr ] are identical ([ is just test with a required closing ]). They are external commands and follow strict POSIX rules.
# String tests
[ -z "$str" ] # true if str is empty (zero length)
[ -n "$str" ] # true if str is non-empty
[ "$a" = "$b" ] # string equal (note: = not ==)
[ "$a" != "$b" ] # string not equal
# Numeric tests (integers only)
[ "$a" -eq "$b" ] # equal
[ "$a" -ne "$b" ] # not equal
[ "$a" -lt "$b" ] # less than
[ "$a" -le "$b" ] # less or equal
[ "$a" -gt "$b" ] # greater than
[ "$a" -ge "$b" ] # greater or equal
# File tests
[ -f "$path" ] # is a regular file
[ -d "$path" ] # is a directory
[ -e "$path" ] # exists (any type)
[ -r "$path" ] # readable
[ -w "$path" ] # writable
[ -x "$path" ] # executable
[ -s "$path" ] # exists and has size > 0
[ -L "$path" ] # is a symbolic link
# Logical
[ expr1 -a expr2 ] # AND (use && instead — safer)
[ expr1 -o expr2 ] # OR (use || instead — safer)
[ ! expr ] # NOTAlways quote variables in [ ]
In [ ], unquoted variables undergo word splitting. If $name is empty or contains spaces, the test breaks:
name=""
[ $name = "alice" ] # ❌ Error: [ = alice ] — "=" has wrong number of args
[ "$name" = "alice" ] # ✅ safe — [ "" = "alice" ]
With [[ ]] quoting is less critical — but still good practice.
[[ ]] — Bash Compound Command
[[ ]] is a Bash (and ksh/zsh) keyword — not an external command. It's safer and more powerful than [ ].
[[ ]] advantages over [ ]
| Feature | [ ] | [[ ]] |
|---|---|---|
| Unquoted variables are safe | ❌ No | ✅ Yes (mostly) |
&& and ` | ` inside | |
Pattern matching (== with *) | ❌ No | ✅ Yes |
Regex matching (=~) | ❌ No | ✅ Yes |
String comparison operators (<, >) | Needs \< | ✅ No escaping |
# String comparison (lexicographic)
[[ "apple" < "banana" ]] # true — no need to escape <
# Pattern matching (glob)
[[ "$filename" == *.txt ]] # true if filename ends with .txt
[[ "$str" == *error* ]] # true if str contains "error"
# Regex matching
[[ "$email" =~ ^[a-z]+@[a-z]+.[a-z]+$ ]] # basic email pattern
echo "${BASH_REMATCH[0]}" # full match
echo "${BASH_REMATCH[1]}" # first capture group
# Combining conditions inside [[ ]]
[[ -f "$file" && "$file" == *.log ]]
[[ "$x" -gt 0 || "$y" -gt 0 ]](( )) — Arithmetic Tests
count=15
if (( count > 10 )); then # arithmetic test — cleaner for numbers
echo "count is greater than 10"
fi
if (( count % 2 == 0 )); then # even check
echo "even"
else
echo "odd"
fi
# No quotes needed — you're inside an arithmetic context
x=5 y=10
if (( x + y == 15 )); then echo "sum is 15"; fiQuick Reference
Which to use?
- Strings and files → use
[[ ]] - Numbers (integers) → use
(( )) - Portable/POSIX sh scripts → use
[ ]with careful quoting
Which test correctly checks if the variable `filename` ends with `.log`?
Write a script file-type.sh that:
- Takes a path as argument
- Checks and prints one of: "regular file", "directory", "symlink", "does not exist"
- Also checks if the file is readable — if it's a regular file that is NOT readable, add " (not readable)"