From test to [[ ]]: Choosing the Right Conditional Construct
Bash offers three ways to evaluate a condition: the test command, the POSIX [ ] builtin, and the Bash-specific [[ ]] keyword. test expr and [ expr ] are functionally identical and behave like any other command, meaning the shell performs word splitting and pathname expansion on unquoted variables inside them, which is a frequent source of bugs. [[ expr ]] is parsed specially by Bash itself, so it never word-splits or globs unquoted variables, supports &&, ||, and </> without escaping, and adds the =~ regex-match operator. For any script that only needs to run under Bash, [[ ]] is the safer default.
Cricket analogy: Choosing [[ ]] over [ ] is like a captain picking DRS over the on-field umpire's naked eye for an lbw shout against Kohli — both can rule, but one has built-in safeguards against a bad call.
String Comparison Operators
Inside [[ ]], strings are compared with = or == for equality, != for inequality, < and > for lexicographic ordering (locale-dependent), -z to test for an empty string, and -n to test for a non-empty one. Because [[ ]] never word-splits, [[ $name == "" ]] is safe even if $name is unset, whereas the same check inside [ ] without quotes — [ $name == "" ] — can collapse to [ == "" ] and throw a syntax error. == inside [[ ]] also enables glob-style pattern matching on the right-hand side, so [[ $file == *.log ]] works without invoking case or grep.
Cricket analogy: Testing -z "$name" is like checking whether a batter's name is even on the scorecard before commentating on their strike rate — you can't compare = 0 if there's no entry at all.
Numeric Comparison Operators
Numeric comparisons use dedicated two-letter operators — -eq, -ne, -lt, -le, -gt, -ge — inside test, [ ], or [[ ]], because = and == always perform string comparison even when both operands look like numbers. A very common bug is writing [ $count = 10 ] expecting a numeric check; it works by accident for 10 but silently misbehaves for values like 010 versus 10 in some contexts, or fails entirely once whitespace sneaks in. For arithmetic-heavy conditionals, (( count > 10 )) is often clearer than [[ $count -gt 10 ]], since it uses familiar C-style operators and automatically treats bare variable names as numbers without needing $.
Cricket analogy: Using -gt instead of > for a run count is like insisting on comparing actual numeric totals — 45 not-out beats 9 — rather than comparing the players' names alphabetically, which would rank 'Ashwin' ahead of 'Kohli' for no cricketing reason.
File Test Operators
File test operators let a script inspect the filesystem before acting on it: -e checks existence of any kind, -f checks it's a regular file, -d checks it's a directory, -L checks it's a symbolic link, -r/-w/-x check read/write/execute permission for the current user, -s checks the file exists and is non-empty, and -nt/-ot compare modification times between two files. These are essential guards before operations like sourcing a config file, writing a log, or running an executable, because attempting the operation on a missing or wrong-type path produces a much less informative error than a clean upfront check does.
Cricket analogy: Checking -f "$pitchReport" before reading it is like a groundsman confirming the pitch report actually exists before the toss, rather than the captain guessing conditions and getting caught out by an unread wicket.
#!/usr/bin/env bash
set -euo pipefail
config="/etc/myapp/config.yml"
logfile="/var/log/myapp.log"
count=${1:-0}
# File tests before acting
if [[ ! -f "$config" ]]; then
echo "Error: config file not found at $config" >&2
exit 1
fi
if [[ -w "$logfile" || ! -e "$logfile" ]]; then
echo "Log target is writable or can be created."
fi
# String test
name=${2:-}
if [[ -z "$name" ]]; then
echo "No name supplied, defaulting to 'guest'."
name="guest"
fi
# Numeric test with (( ))
if (( count >= 10 )); then
echo "$name triggered a high-count alert: $count"
elif [[ $count -gt 0 ]]; then
echo "$name has a moderate count: $count"
fi
# Pattern match with ==
if [[ $config == *.yml ]]; then
echo "Config is YAML formatted."
fiNever write [ $var = value ] without quoting $var. If the variable is empty or unset, the shell reduces this to [ = value ], which is a syntax error; if it contains spaces or glob characters, word splitting and pathname expansion can silently change the comparison. Always write [ "$var" = value ], or switch to [[ $var = value ]], which is immune to word splitting even unquoted.
[[ ]]is a Bash keyword that avoids word splitting and globbing;[ ]/testare ordinary commands that do not.- Use
=,!=,-z,-nfor strings and-eq,-ne,-lt,-le,-gt,-gefor numbers — never mix the two. [[ $x == pattern ]]supports glob matching on the right-hand side;=~adds full regex matching.(( expr ))uses familiar C-style numeric operators (>,<,>=) and treats bare variable names as numbers.- File test operators (
-e -f -d -L -r -w -x -s -nt -ot) let you validate a path before acting on it. - Always quote variables inside
[ ]to avoid syntax errors on empty values or corrupted comparisons from word splitting. - Prefer
[[ ]]for Bash-only scripts; reserve[ ]/testfor scripts that must remain POSIX-sh portable.
Practice what you learned
1. Which construct is immune to word splitting and pathname expansion on unquoted variables?
2. What is wrong with `[ $count = 10 ]` when the intent is a numeric comparison?
3. Which file test operator checks that a path exists and is a regular file (not a directory)?
4. Why can `[ $name = "" ]` throw a syntax error if `$name` is unset?
5. Inside `[[ ]]`, what does `[[ $file == *.log ]]` do?
Was this page helpful?
You May Also Like
Case Statements in Depth
Learn how Bash's case statement uses glob patterns, fall-through terminators, and pattern lists to replace long if/elif chains.
Advanced Loop Patterns
Go beyond basic for loops with C-style iteration, while-read idioms for safe line processing, and loop control with break, continue, and select.
Command Substitution in Depth
Understand how $(...) captures command output as a string, why it's preferred over backticks, and the quoting rules that prevent word-splitting bugs.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAnsible Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics