test, [, and [[ ]] Expressions
Shell condition syntax can look inconsistent because three related forms coexist: the external-or-builtin-style `test`, the `[` command, and Bash's richer `[[ ... ]]` conditional construct. They overlap, but they do not parse words the same way or offer the same safety and features.
Learning objectives
By the end of this lesson
- Explain the relationship between test and [.
- Use [[ ... ]] safely for Bash-specific conditions.
- Understand quoting and word-splitting differences.
- Apply pattern matching and regular expressions deliberately.
- Choose between portable POSIX tests and Bash-native conditionals.
1. Three forms, two parsing models
test expressionCommand/builtin stylePortable foundation; arguments are ordinary shell words[ expression ]Command/builtin styleEquivalent family to test; closing ] is a required argument[[ expression ]]Bash conditional constructSpecial shell syntax with safer string handling and Bash features[ is not decorative punctuation around arbitrary Bash expressions. It behaves as a command name, and the closing bracket terminates its argument list.
name="academy"
test "$name" = "academy" && printf 'test matched\n'
[ "$name" = "academy" ] && printf '[ matched\n'
[[ $name == academy ]] && printf '[[ matched\n'2. Spaces are syntax in [ ... ]
Because [ receives arguments, whitespace separates its tokens. Missing spaces changes the command line entirely.
value="yes"
# Correct:
if [ "$value" = "yes" ]; then
printf 'yes\n'
fi
# Incorrect examples:
# [ "$value"="yes" ]
# ["$value" = "yes"]The first incorrect expression can become a one-argument non-empty-string test rather than the comparison you intended. The second tries to execute a command whose name begins with [yes.
3. Quote expansions carefully with test and [
In classic test syntax, unquoted expansions can disappear or split into multiple arguments before [ receives them.
name="two words"
if [ "$name" = "two words" ]; then
printf 'safe comparison\n'
fiWhen using test or [, quote variable expansions unless you have a specific documented reason not to.
4. [[ ... ]] suppresses word splitting and pathname expansion for ordinary operands
Inside Bash's [[ ... ]], parameter expansions used as operands do not undergo the same word splitting and filename expansion that makes classic test syntax fragile.
name="two words"
pattern="*.log"
[[ $name == "two words" ]] && printf 'string matched\n'
[[ $pattern == "*.log" ]] && printf 'literal pattern text matched\n'You should still quote when you mean a literal string, especially on the right-hand side of operators whose behavior changes when quoted.
5. == inside [[ ]] can perform pattern matching
In [[ value == pattern ]], an unquoted right-hand side is interpreted as a shell pattern.
file="deploy-prod.yaml"
if [[ $file == deploy-*.yaml ]]; then
printf 'deployment YAML name matched\n'
fi
literal="deploy-*.yaml"
if [[ $file == "$literal" ]]; then
printf 'this would require literal equality\n'
fiQuoting the right-hand side turns the wildcard characters into ordinary text for this comparison.
6. =~ provides Bash regular-expression matching
The =~ operator compares the left string against a POSIX extended regular expression.
version="v2.14.7"
if [[ $version =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
printf 'major=%s minor=%s patch=%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
fiBASH_REMATCH[0] contains the full match, while later elements contain captured groups. Regex quoting rules can be subtle; put complex regular expressions in variables when that improves readability.
semver_re='^v([0-9]+)\.([0-9]+)\.([0-9]+)$'
[[ $version =~ $semver_re ]]7. Use &&, ||, and ! inside [[ ]] for compound tests
Bash conditional expressions can combine predicates directly:
environment="staging"
replicas=3
if [[ $environment == staging && $replicas -gt 0 ]]; then
printf 'staging configuration is deployable\n'
fi
if [[ ! $environment == prod ]]; then
printf 'not production\n'
fiThis is different from shell-level && and || connecting separate commands, though both ultimately participate in status-based control flow.
8. [[ ]] supports grouped conditional logic
Parentheses can group subexpressions inside [[ ... ]]. Quote or escape them appropriately if you are writing classic [ ... ] expressions because there they are ordinary shell tokens with different parsing implications.
role="worker"
environment="prod"
if [[ ( $role == api || $role == worker ) && $environment == prod ]]; then
printf 'production workload role accepted\n'
fi9. Choose portability intentionally
If a script declares #!/usr/bin/env bash, using [[ ... ]] is usually appropriate and often safer. If a script must run under a generic POSIX sh, [[ ... ]] and =~ cannot be assumed.
#!/bin/sh
# Portable style:
if [ "$MODE" = "production" ]; then
printf '%s\n' "production"
fi
#!/usr/bin/env bash
# Bash-specific style:
if [[ $MODE == prod* ]]; then
printf '%s\n' "production-like mode"
fiPortability begins with the shebang and deployment environment. Do not write Bash-only syntax in a script advertised as POSIX sh.
10. Prefer explicit composition over -a and -o in classic test
Historical test/[ forms include logical operators such as -a and -o, but expressions can become ambiguous across argument counts and implementations. Prefer separate tests connected by shell &&/||, or use [[ ... ]] in Bash.
# Clear portable composition:
if [ -r "$config" ] && [ -s "$config" ]; then
printf 'readable non-empty config\n'
fi
# Clear Bash composition:
if [[ -r $config && -s $config ]]; then
printf 'readable non-empty config\n'
fi11. Hands-on lab: validate an image tag
Build a Bash-only validator that accepts a deployment environment and an image tag. Use pattern matching for the environment and a regular expression for the tag.
mkdir -p "$HOME/devops-academy/bash/chapter04/lesson02"
cd "$HOME/devops-academy/bash/chapter04/lesson02"
cat > validate.sh <<'EOF'
#!/usr/bin/env bash
environment=${1:-}
tag=${2:-}
tag_re='^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$'
if [[ -z $environment || -z $tag ]]; then
printf 'usage: %s ENVIRONMENT IMAGE_TAG\n' "$0" >&2
exit 64
fi
if [[ $environment != dev && $environment != staging && $environment != prod ]]; then
printf 'invalid environment: %s\n' "$environment" >&2
exit 2
fi
if [[ ! $tag =~ $tag_re ]]; then
printf 'invalid image tag: %s\n' "$tag" >&2
exit 3
fi
if [[ $environment == prod && $tag == latest ]]; then
printf 'production must use an immutable tag\n' >&2
exit 4
fi
printf 'valid: env=%s tag=%s\n' "$environment" "$tag"
EOF
bash validate.sh staging latest
bash validate.sh prod latest || true
bash validate.sh prod 2026.08.09Verification checklist
12. Knowledge check
Question 1. Is [ merely punctuation?
Question 2. What special behavior does an unquoted right side of == have inside [[ ]]?
Question 3. Which Bash operator performs regular-expression matching?
=~.Question 4. Should a script with #!/bin/sh assume [[ ... ]] exists?
[[ ... ]] is not portable POSIX sh syntax.13. Summary
test and [ are classic command-style condition evaluators and require careful quoting. Bash's [[ ... ]] is a richer conditional construct that avoids ordinary word splitting and supports pattern and regex matching. Choose the form that matches your interpreter contract, and write compound logic so its meaning is obvious.
14. Further reading
- GNU Bash Reference Manual — Bash Conditional Expressions and Conditional Constructs.
- POSIX
testutility specification. - GNU Bash Reference Manual — Pattern Matching.
- ShellCheck documentation — conditional-expression diagnostics.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.