| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; no root required for the examples |
| Scope | Comparison of [ ] / test, Bash [[ ]], and (( )), including quoting, word splitting, glob patterns, regex, string and numeric comparisons, file tests, logical operators, and POSIX portability. Does not cover full if/elif grammar, every file-test flag, or complete regex capture syntax. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash [ ], [[ ]], and (( )) can all appear in conditions, but they follow different parsing and comparison rules.
Choosing the wrong form can break on empty or multi-word variables, compare strings when you meant to compare numbers, or turn a regex into a literal match. Bash conditionals ultimately test exit status: status 0 is true and any nonzero value is false.
[ ] vs [[ ]] vs (( )) — Quick Decision
| Your goal | Use | Why |
|---|---|---|
POSIX sh portability |
[ ] or test |
Defined by POSIX; works in Dash and BusyBox sh |
| Bash strings, files, patterns, or regex | [[ ]] |
Bash syntax; no word splitting/pathname expansion on operands; supports patterns and =~ |
| Bash integer arithmetic conditions | (( )) |
Arithmetic evaluation; < and > compare numbers, not text |
Use [ ] when the script must run under /bin/sh on systems that are not Bash. Use [[ ]] for most Bash string and file tests. Use (( )) when the condition is numeric and fits integer arithmetic.
File tests such as -f, -d, and -e work in both [ ] and [[ ]]. Quote path variables in [ ] tests; [[ ]] does not split unquoted expansions into multiple words. For full file-test workflows, see check if a file exists in Bash.
For practical branching with if, elif, and else, see Bash if else.
How [ ] / test Works
Bash provides test and [ as closely related builtins. The [ form behaves like a command whose arguments make up the test expression, and ] must appear as its separate final argument.
Spaces are required because [ and ] are separate shell tokens: write [ "$x" = foo ], not ["$x"=foo].
Confirm that both names resolve to builtins in this shell:
type testtest is a shell builtinBoth invocations run inside Bash itself rather than as an external binary.
The bracket form provides the same test using [ and a required closing ]:
type '['[ is a shell builtinThese two tests are equivalent:
test -n foo && echo okokA nonempty string makes test -n succeed, so the chained echo runs.
The bracket form is the same test with a closing ] as the final argument:
[ -n foo ] && echo bracket-okbracket-okBoth forms perform the same nonempty-string test.
Because [ is a command, unquoted variables undergo word splitting and pathname expansion like any other command argument. That is the root cause of many [ failures covered later.
How [[ ]] Works
[[ ... ]] is Bash conditional expression syntax, not a command named [. Words inside [[ ]] do not undergo word splitting or pathname expansion. [[ and ]] are separate shell keywords too: write [[ $x == foo ]], not [[x==foo]].
A pattern on the right of == matches the left-hand string. Test a value with an embedded space:
bash -c 'x="a b"; [[ $x == a* ]] && echo pattern-ok'pattern-ok[[ ]] kept a b as one word, so the a* pattern matched.
The same test written with [ breaks because $x expands into separate words:
bash -c 'x="a b"; [ $x = a ]' 2>&1bash: line 1: [: too many arguments$x expanded into two separate words, changing the number and positions of the arguments passed to [, so the expression was no longer a valid string comparison.
[[ ]] also accepts the unary and binary file tests (-f, -d, -e, and the rest) and the string operators documented in the GNU Bash manual.
How (( )) Works
(( expression )) is an arithmetic command. It evaluates an integer expression and sets exit status 0 when the result is nonzero, 1 when the result is zero.
Test a numeric comparison:
(( 10 > 5 )) && echo arith-okarith-ok10 > 5 evaluates to true in arithmetic, so the command exits 0 and the echo runs.
(( )) can update variables (((n++)), ((total += 5))) and is closely related to $(( )) arithmetic expansion. For operators, increment forms, and decimal work with awk or bc, see Bash arithmetic.
Word Splitting and Globbing Differences
Inside [ ], the shell expands variables and globs before test runs. Inside [[ ]], pattern matching on == is deliberate; pathname expansion does not rewrite the right-hand side the way it would in a normal command.
With [ ], an unquoted value such as *.txt undergoes pathname expansion before [ receives its arguments — Bash globbing shows when globs expand versus when [[ ]] treats * as a pattern. If multiple matching files exist, one variable expansion can become several arguments and break the test.
[[ ]] is the usual choice when you want shell pattern matching on a filename or other string:
bash -c 'file=report.txt; if [[ $file == *.txt ]]; then printf "matched\n"; fi'matchedreport.txt matched the *.txt pattern on the right of ==.
Quote variables in [ ] tests when you mean literal string comparison. Prefer [[ ]] in Bash-only scripts when patterns or safer parsing matter.
String Comparisons and Pattern Matching
[[ ]] supports == and = as string equality tests. On the right of ==, unquoted text is a shell pattern (*, ?, […]), not a regular expression. Quote the right-hand side when you want literal equality instead of pattern matching: [[ $x == "*.txt" ]] matches the literal text *.txt.
For equality, empty-string, and lexical ordering detail, see Bash compare strings.
Test whether a string is empty with either bracket form:
[[ -z "" ]] && echo empty-okempty-ok-z returns true when the tested string has zero characters.
The [ form works the same way for this unary test:
[ -z "" ] && echo empty-bracket-okempty-bracket-okWith [ ], quote variable expansions used as operands so empty strings or spaces do not change the argument structure. Inside [[ ]], word splitting and pathname expansion are not performed on variable expansions.
Inside [[ ]], < and > are string comparison operators directly. Bash's [ ] builtin also supports them, but they must be escaped or quoted so the shell does not treat them as redirections, for example [ "$a" \< "$b" ]. Do not rely on these operators when writing strictly portable POSIX sh; use = / != for strings or the numeric -lt / -gt operators where appropriate.
Numeric Comparisons
[[ ]] provides integer operators -eq, -ne, -lt, -le, -gt, and -ge when both sides should be treated as numbers:
[[ "2" -eq 2 ]] && echo eq-okeq-ok-eq compares integers, so the quoted string 2 and the number 2 test as equal.
Inside [[ ]], the < and > operators perform lexical string comparison:
[[ "10" < "2" ]] && echo lex-true || echo lex-falselex-true[[ ]] performs lexical ordering using the current locale. In this example, "1" sorts before "2", so "10" is considered less than "2".
(( )) uses C-like arithmetic operators, so the same values compare as integers:
(( 10 < 2 )) && echo arith-true || echo arith-falsearith-falseIn arithmetic, 10 is not less than 2, so (( )) exits 1 and the else branch prints arith-false.
For script examples with -eq and related flags, see Bash compare numbers.
Regex Matching
[[ string =~ regex ]] matches the string against a POSIX extended regular expression. Quoting any part of the regex makes that quoted portion match literally.
Match digits at the end of a string:
[[ "abc123" =~ ^[a-z]+[0-9]+$ ]] && echo regex-okregex-okThe =~ operator matched the ERE: lowercase letters followed by digits.
For practical contains checks, including any digit, digits only,
alphanumeric text, prefixes, and suffixes, see how to
test what a Bash string contains.
Quoting the whole regex turns metacharacters into literal characters:
[[ "abc123" =~ '^[a-z]+[0-9]+$' ]] && echo quoted-match || echo quoted-no-matchquoted-no-matchThe quoted pattern looked for literal ^[a-z]+[0-9]+$ text, not a regex structure.
Regex capture with BASH_REMATCH belongs in Bash regex; this article stops at the basic =~ form.
Logical Operators and Grouping
All three forms can be used in conditions, but && and || are parsed differently depending on the form. [[ ]] and (( )) support &&, ||, and ! inside their expressions. With portable [ ], combine separate test commands with the shell's && and || operators.
Do not write [ "$a" = x && "$b" = y ]. That tries to pass && as a test argument. Use [[ $a = x && $b = y ]] in Bash, or chain two [ ] commands in portable scripts.
Older scripts may use -a and -o inside [ ]. Avoid them in new code; combine separate [ ] commands with shell && or || instead.
Typical Bash style chains [[ ]] expressions:
file=/etc/passwdSet a path variable, then require a regular file that is readable:
[[ -f "$file" && -r "$file" ]] && echo readable-filereadable-fileHere && is part of the [[ ]] conditional expression, not a shell list operator between two commands.
With [ ], chain separate bracket commands:
[ -f "$file" ] && [ -r "$file" ] && echo posix-readableposix-readableHere && is a shell list operator between two [ commands.
(( )) combines arithmetic tests inside one expression:
count=3With count set, combine bounds in one arithmetic command:
(( count > 0 && count < 10 )) && echo in-rangein-rangeInside (( )), && follows arithmetic expression rules. count is between 0 and 10, so the combined test exits 0.
POSIX Portability
For conditional tests, portable POSIX sh provides test / [ ]. The [[ ... ]] conditional and the (( expression )) arithmetic command are extensions used by Bash and some other shells. POSIX does support $(( expression )) arithmetic expansion, which is a different construct.
Scripts that must run on Dash, BusyBox ash, or other POSIX shells should use [ ], quote variables, and avoid =~. For a full portability checklist, see Bash vs sh.
Side-by-Side Failure Examples
These examples show how unquoted empty or multi-word expansions can change the argument structure of [ ], while [[ ]] avoids word splitting and pathname expansion.
An unset variable with [ collapses to a unary operator error:
bash -c 'unset var; [ $var = x ]; printf "exit=%s\n" "$?"' 2>&1bash: line 1: [: =: unary operator expected
exit=2With var unset, the unquoted $var contributed no argument, so the command effectively became [ = x ]. Status 2 means a usage error, not a simple “condition false” you can treat like 1.
With default shell options, [[ ]] handles an unset variable differently:
bash -c 'unset var; [[ $var = x ]] && echo match || echo no-match'no-matchWith the default shell options, [[ $var = x ]] treats an unset var like an empty string and returns false without a syntax error. If set -u is enabled, referencing an unset variable is an error.
| Symptom | Likely cause | Fix |
|---|---|---|
[: too many arguments |
Unquoted variable split into multiple words | Quote: [ "$x" = y ], or use [[ ]] |
[: =: unary operator expected |
Unquoted empty variable disappeared, changing the test's argument structure | Quote variables, or use [[ ]] |
"10" < "2" is true in [[ ]] |
Lexical <, not numeric |
Use -lt / -gt in [[ ]], or (( )) |
=~ regex does not match as expected |
Regex operand was quoted, making quoted metacharacters literal | Leave regex syntax unquoted; quote only portions that should be literal |
Which Form Should New Bash Scripts Use?
For new Bash-only scripts, default to [[ ]] for string and file conditions and (( )) for integer arithmetic. Keep [ ] when the script must remain portable to POSIX sh.
[[ ]]— everyday Bash conditionals, patterns, and=~regex(( ))— counters, numeric thresholds, and arithmetic loops[ ]/test— scripts that must remain portable to POSIXsh
Summary
Use [ ] or test when the script must run under POSIX sh. Use [[ ]] for Bash string, file, pattern, and regex tests. Use (( )) for integer arithmetic conditions. [ ] parses operands like ordinary command arguments; [[ ]] and (( )) follow Bash conditional and arithmetic rules instead.
Empty or multi-word values break unquoted [ tests. Lexical < in [[ ]] is not numeric ordering, and quoting a =~ operand makes regex metacharacters literal. For portable scripts, stay with [ ], quote every operand, and avoid Bash-only operators such as =~.
References
- GNU Bash manual — Bash Conditional Expressions —
[[ ]]operators and=~ - GNU Bash manual — Conditional Constructs —
(( ))andif - POSIX test utility (Issue 8) — portable
[ ]/testbehavior
Frequently Asked Questions
Is [[ ]] safer than [ ]?
In Bash, [[ ]] is less error-prone for typical string tests because variable expansions are not subject to word splitting or pathname expansion. Unquoted variables are less likely to trigger too many arguments or unary operator expected. [ is still the right choice when you need POSIX sh portability, with appropriate quoting of variable expansions.
Is [[ ]] POSIX?
No. [[ ]] is a Bash (and Ksh-derived) extension. POSIX sh defines only test and [ ]. Use [ ] in scripts that must run under Dash or BusyBox sh.
Can I compare numbers with [[ ]]?
Yes, with integer operators such as -eq, -ne, -lt, -le, -gt, and -ge. Do not use bare < or > in [[ ]] when you mean numeric order — those operators compare strings lexically. For pure arithmetic conditions, (( )) is often clearer.
Why does [ $var = x ] break when empty?
When var is unset or empty, the unquoted $var contributes no argument, so the command effectively becomes [ = x ]. That is not a valid two-argument test, so Bash prints unary operator expected and returns status 2. Quote the variable ([ "$var" = x ]) or use [[ $var = x ]] in Bash-only scripts.

