test and [[ ]] - Conditionals in Shell Scripts
What You'll Learn
- The difference and trade-offs between
test,[ ], and[[ ]] - How to pick the right operators for strings, numbers, and files
- How to structurally avoid quoting bugs like
[: too many arguments
Quick Summary
- When in doubt, use
[[ ]](the safe default in bash) - Use
[ ](i.e.test) only when you need POSIXshportability - Almost every bug comes from (1) missing quotes or (2) mixing up operators (
-eqvs=)
Assumptions
- Shell: bash (
[[ ]]is a bash / ksh / zsh extension; it does not exist in POSIXsh) - Scripts start with
#!/bin/bash
What's the difference between test, [ ], and [[]]?
Conclusion:
[ ]is literally thetestcommand.[[ ]]is a shell keyword with safer quoting, more operators, and pattern matching.
Here is the relationship in one table.
| Syntax | What it really is | Portability | Main strength |
|---|---|---|---|
test EXPR |
A builtin (also exists on disk) | POSIX (high) | Most fundamental |
[ EXPR ] |
An alias for test (] is an arg) |
POSIX (high) | Reads well with if |
[[ EXPR ]] |
A shell keyword | bash etc. (limited) | Quote-safe, =~, &&/|| allowed |
Because [ ] is a command, you need spaces around [, ], and every operator. test and [ behave identically.
$ test 1 -lt 2; echo $? 0 $ [ 1 -lt 2 ]; echo $? 0
[[ ]], by contrast, is parsed as shell syntax, which is what makes it quote-safe and capable of pattern matching.
Confirm what [ really is
$ type [ [ is a shell builtin $ ls -l /usr/bin/[ -rwxr-xr-x 1 root root ... /usr/bin/[
The builtin takes priority, but test/[ also exist as external commands for historical reasons.
How do you compare strings?
Conclusion: Use
=for equality (==also works inside[[ ]]),-zfor empty and-nfor non-empty. Always double-quote variables.
Core operators
=... equal (==is a synonym inside[[ ]])!=... not equal-z "$s"... string is empty (zero length)-n "$s"... string is not empty
name="penguin"
if [[ "$name" = "penguin" ]]; then
echo "match"
fi
if [[ -z "$name" ]]; then
echo "empty"
else
echo "not empty"
fi= vs ==
== is a bash extension. For POSIX sh portability, use = inside [ ]. Inside [[ ]] both work.
[ "$a" = "$b" ] # POSIX-safe [[ "$a" == "$b" ]] # bash (both = and == work inside [[ ]])
How do you compare numbers?
Conclusion: Use
-eq -ne -lt -le -gt -gefor numbers.=and>are string operators and mean something different.
| Operator | Meaning | Math symbol |
|---|---|---|
-eq |
equal | == |
-ne |
not equal | != |
-lt |
less than | < |
-le |
less than or equal | <= |
-gt |
greater than | > |
-ge |
greater than or equal | >= |
count=42
if [[ "$count" -ge 10 ]]; then
echo "10 or more"
fiDo not mix up = and -eq
[[ "08" = "8" ]] # false (different as strings) [[ "08" -eq "8" ]] # true (equal as numbers)
Using = where you mean a numeric compare leads to surprises with zero-padding or whitespace.
If you want arithmetic, (( )) lets you write math symbols directly: (( count >= 10 )).
How do you test whether a file exists or its type?
Conclusion: File test operators check existence, type, and permissions. The common ones are
-e -f -d -r -w -x -s.
| Operator | Meaning |
|---|---|
-e file |
exists (any type) |
-f file |
is a regular file |
-d file |
is a directory |
-r file |
is readable |
-w file |
is writable |
-x file |
is executable |
-s file |
size is greater than zero |
-L file |
is a symbolic link |
a -nt b |
a is newer than b |
config="/etc/myapp.conf"
if [[ -f "$config" && -r "$config" ]]; then
echo "readable config file exists"
else
echo "config missing or unreadable"
fiTest for "does not exist"
Negate with !. A common early-return pattern.
if [[ ! -d "$dir" ]]; then
echo "No such directory: $dir" >&2
exit 1
fiWhy is [[]] less error-prone?
Conclusion: Inside
[[ ]], expanded variables are not subject to word splitting or globbing, so a missing quote does not break the syntax.
Since [ ] is a command, an empty or whitespace-containing variable changes the argument count and breaks the expression.
v="" [ $v = "x" ] # becomes [ = "x" ]: too many arguments / syntax error [ "$v" = "x" ] # becomes [ "" = "x" ] (correctly false) - quoting saves you v="a b" [ $v = "x" ] # becomes [ a b = "x" ] (4 args, error)
Inside [[ ]], an expanded variable is not split or globbed, so even a missing quote rarely breaks anything.
v="a b" [[ $v = "x" ]] # does not break (false)
Still, keep the habit of quoting. Writing "$var" in both [ ] and [[ ]] keeps the code safe no matter which you port it to.
What are [[]]'s pattern matching and regex?
Conclusion: Inside
[[ ]], the right side of==is a glob pattern and=~is a regular expression. Neither exists in[ ].
Glob pattern matching (==)
Leave the right side unquoted for a pattern; quote it to match it as a literal string.
file="report.txt"
if [[ "$file" == *.txt ]]; then
echo "text file"
fi
if [[ "$file" == "*.txt" ]]; then
echo "only matches the literal string *.txt"
fiRegex matching (=~)
Leave the right side unquoted. Captured groups land in the BASH_REMATCH array.
input="port=8080"
if [[ "$input" =~ ^port=([0-9]+)$ ]]; then
echo "port number: ${BASH_REMATCH[1]}"
fiIf you double-quote the right side of =~, it is treated as a literal string, not a regex (bash 3.2+). The safe form is to put the pattern in a variable: [[ "$s" =~ $re ]].
How do you combine multiple conditions?
Conclusion: Inside
[[ ]]use&&and||. With[ ], join separate commands:[ ... ] && [ ... ].
# Combine inside [[ ]] (readable)
if [[ "$age" -ge 18 && "$age" -lt 65 ]]; then
echo "working age"
fi
# Join [ ] commands with &&
if [ "$age" -ge 18 ] && [ "$age" -lt 65 ]; then
echo "working age"
fiThe -a (AND) and -o (OR) operators inside [ ] are deprecated. Their argument parsing is ambiguous and a frequent source of bugs. To combine conditions, split into separate [ ] and join with && / ||.
Summary: when to use test vs [[]]
Conclusion: In bash scripts, default to
[[ ]]and reserve[ ]for places that need POSIXsh. Get quoting and operator choice right and bugs nearly disappear.
[[ ]]... bash only. Quote-safe, supports=~,&&/||, and reads well[ ]/test... when you need#!/bin/shor POSIX compatibility- Strings use
=/-z/-n, numbers use the-eqfamily, files use-f/-d - Always quote variables as
"$var"
Copy-paste template
# Argument check
if [[ $# -lt 1 ]]; then
echo "usage: $0 <file>" >&2
exit 1
fi
target="$1"
# File existence and type
if [[ ! -f "$target" ]]; then
echo "not a regular file: $target" >&2
exit 1
fi
# Branch on extension
if [[ "$target" == *.log ]]; then
echo "processing a log file"
fi