Test Expressions ([ ] and [[ ]])
Every if statement, while loop, and conditional check in Bash relies on a test expression to decide which branch to take. Test expressions are how you ask questions like "does this file exist?", "is this number greater than 10?", or "are these two strings equal?". Bash gives you two ways to write them: the classic [ ] (a thin wrapper around the test command) and the Bash-specific [[ ]], which is safer and more powerful. Understanding both — and knowing which to reach for — is essential to writing scripts that behave correctly.
Overview / How it works
In Bash, conditionals like if and while don’t evaluate a boolean value the way other languages do. Instead, they run a command and check its exit status: 0 means success (true), and any non-zero value means failure (false). test is a real command — you can run test -f /etc/hosts directly, and it exits 0 if the file exists or 1 if it doesn’t. It prints nothing; its only output is its exit status.
[ is simply another name for the same test command — on most systems it exists both as a shell builtin and as a real executable at /usr/bin/[. Because [ is a command, the closing ] is actually its last argument, not special syntax. That’s why [ -f /etc/hosts ] requires a space before the closing bracket: the shell needs to see ] as its own word, exactly like any other argument. Since [ is an ordinary command, its arguments go through normal word splitting and pathname expansion (globbing) before test ever sees them — which is the root cause of most [ ] bugs.
[[ ]] is different: it’s a Bash keyword (a piece of shell grammar), not a command. Because the shell parses it specially, variables inside [[ ]] are not word-split or glob-expanded even when unquoted, &&/|| work directly inside it, and it supports glob and regex pattern matching that [ ] can’t do safely. The tradeoff is that [[ ]] is Bash-only — it won’t work in a POSIX sh or dash script, while [ ]/test works everywhere. Since this course targets Bash specifically, [[ ]] is the recommended default for new scripts; you’ll still see [ ] constantly in real-world code and need to read it fluently.
Syntax
test EXPRESSION
[ EXPRESSION ]
[[ EXPRESSION ]]
All three forms evaluate EXPRESSION and set the exit status accordingly. The table below lists the operators you’ll use most.
| Category | Operator | Meaning |
|---|---|---|
| String | = or == |
strings are equal (== only inside [[ ]]) |
| String | != |
strings are not equal |
| String | -z |
string is empty (zero length) |
| String | -n |
string is not empty |
| String | < / > |
lexicographic less-than / greater-than ([[ ]] only; must be escaped as \</\> in [ ]) |
| Numeric | -eq / -ne |
equal / not equal |
| Numeric | -lt / -le |
less than / less than or equal |
| Numeric | -gt / -ge |
greater than / greater than or equal |
| File | -e |
path exists (any type) |
| File | -f |
path exists and is a regular file |
| File | -d |
path exists and is a directory |
| File | -r / -w / -x |
path is readable / writable / executable |
| File | -s |
path exists and has a size greater than zero |
| Logical | ! |
negates the result |
| Logical | && / || |
AND / OR (inside [[ ]]; use -a/-o or separate [ ] tests joined by shell &&/|| for [ ]) |
Examples
Example 1: checking whether a directory exists before writing a backup into it.
#!/usr/bin/env bash
BACKUP_DIR="/var/backups/website"
if [ -d "$BACKUP_DIR" ]; then
echo "Backup directory exists, proceeding."
else
echo "Creating backup directory..."
mkdir -p "$BACKUP_DIR"
fi
Output:
Creating backup directory...
The script tests the -d (directory) condition. Because /var/backups/website didn’t exist yet, [ -d "$BACKUP_DIR" ] exited non-zero, so the else branch ran and created it with mkdir -p.
Example 2: a numeric comparison that classifies disk usage into severity levels.
#!/usr/bin/env bash
USAGE=87
if [ "$USAGE" -ge 90 ]; then
echo "CRITICAL: disk usage at ${USAGE}%"
elif [ "$USAGE" -ge 80 ]; then
echo "WARNING: disk usage at ${USAGE}%"
else
echo "OK: disk usage at ${USAGE}%"
fi
Output:
WARNING: disk usage at 87%
-ge compares the two values as integers, not as text, so 87 -ge 90 is false but 87 -ge 80 is true. Using = or == here instead of -ge would be a mistake — those operators compare text, not magnitude.
Example 3: using [[ ]] for string comparison and pattern matching, something plain [ ] cannot do safely.
#!/usr/bin/env bash
read -rp "Enter environment (dev/staging/prod): " ENV
if [[ "$ENV" == "prod" || "$ENV" == "staging" ]]; then
echo "Deploying to a shared environment: $ENV"
elif [[ "$ENV" == dev* ]]; then
echo "Deploying to a local dev environment: $ENV"
else
echo "Unknown environment: $ENV"
fi
Output (when the user types prod):
Deploying to a shared environment: prod
Inside [[ ]], dev* is treated as a glob pattern (matching any string starting with dev) rather than a literal filename, because the right-hand side of == in [[ ]] is not word-split or glob-expanded against the filesystem. The same pattern used unquoted inside [ ] would behave unpredictably.
How it works step by step
When Bash hits if [ -d "$BACKUP_DIR" ]; then, it does the following:
- Bash expands
"$BACKUP_DIR"into its current value, keeping it as a single word because it’s quoted. - Bash runs the command
[with the arguments-d, the expanded path, and]. test(invoked as[) checks the filesystem, sets its exit status to0or1, and produces no output.ifinspects that exit status:0runs thethenblock, anything else moves toelif/else.
With [[ ]] the process is similar, except step 2 never happens as a separate command invocation — the shell’s parser evaluates the expression directly as part of the if statement’s grammar, which is why quoting matters less (though it’s still good practice) and why patterns and regexes can be handled specially.
Common Mistakes
Mistake 1: missing the required spaces around the brackets. [ and ] must be surrounded by whitespace because they are separate words/arguments, not punctuation.
[$a -eq $b]
This is parsed as a command literally named [$a, which doesn’t exist, so Bash reports "command not found". The fix is to always leave a space after [ and before ]:
[ "$a" -eq "$b" ]
Mistake 2: leaving a variable unquoted inside [ ]. If the variable is empty or contains spaces, word splitting changes how many arguments test receives.
FILE_NAME="my report.txt"
if [ $FILE_NAME = "my report.txt" ]; then
echo "Match"
fi
Because $FILE_NAME is unquoted, it splits into two words (my and report.txt), and [ receives too many arguments, producing an error instead of a comparison. Quoting fixes it:
FILE_NAME="my report.txt"
if [ "$FILE_NAME" = "my report.txt" ]; then
echo "Match"
fi
Mistake 3: using a numeric operator on strings, or a string operator on numbers. -eq tries to parse both sides as integers.
ROLE="admin"
if [ "$ROLE" -eq "admin" ]; then
echo "Is admin"
fi
This fails with integer expression expected, because -eq can’t parse the text admin as a number. The correct operator for string equality is = (or == inside [[ ]]):
ROLE="admin"
if [ "$ROLE" = "admin" ]; then
echo "Is admin"
fi
Best Practices
- Prefer
[[ ]]in Bash scripts — it avoids word-splitting and glob surprises and supports pattern matching and&&/||directly. - Always quote variable expansions inside
[ ](and as a habit inside[[ ]]too), e.g.[ -f "$path" ], not[ -f $path ]. - Use
-eq/-ne/-lt/-le/-gt/-geonly for integers; use=/!=for strings. - Leave a space immediately after
[/[[and immediately before]/]]. - Test the specific file type you actually need (
-ffor a file,-dfor a directory) rather than the generic-e, so you don’t accidentally act on the wrong kind of path. - If a script needs to run under plain POSIX
shas well as Bash, stick to[ ]/testand avoid[[ ]], since[[ ]]is a Bash (and some other shells’) extension, not POSIX. - Check
help testorman testwhen unsure exactly what an operator does — don’t guess.
Practice Exercises
- Write a script that takes a path as
$1and prints whether it’s a regular file, a directory, or doesn’t exist at all, using-f,-d, and-e. - Write a script with a variable
ATTEMPTS=2that printsretryifATTEMPTSis less than3, andgive upotherwise. - Write a script using
[[ ]]that checks whether a variableFILENAMEends in.logusing a glob pattern (hint:[[ "$FILENAME" == *.log ]]), and prints a different message if it doesn’t.
Summary
[ ]is thetestcommand; the closing]is its final argument, so spacing and quoting matter a lot.[[ ]]is a Bash keyword: safer with unquoted variables, and it supports pattern matching and&&/||directly, but it’s Bash-only, not POSIX.- Use
=/!=(or==in[[ ]]) for strings, and-eq/-ne/-lt/-le/-gt/-gefor integers — mixing them up produces errors or wrong results. - File test operators like
-e,-f, and-dlet you check a path’s existence and type before acting on it. - Always quote variable expansions inside test expressions to avoid word-splitting bugs, especially with
[ ].
