Sagar.BlogArticle
All posts
All posts
Bash

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.

January 11, 20268 min read
BashTestConditionalsScripting

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 ]           # NOT

Always 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"; fi

Quick Reference

Which to use?

  • Strings and files → use [[ ]]
  • Numbers (integers) → use (( ))
  • Portable/POSIX sh scripts → use [ ] with careful quoting
Quick Check

Which test correctly checks if the variable `filename` ends with `.log`?

Exercise

Write a script file-type.sh that:

  1. Takes a path as argument
  2. Checks and prints one of: "regular file", "directory", "symlink", "does not exist"
  3. Also checks if the file is readable — if it's a regular file that is NOT readable, add " (not readable)"