| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user |
| Scope | String equality and inequality, empty and non-empty tests, unset versus empty variables, substring and glob matching on ==, [ ] versus [[ ]], case-insensitive equality, and lexicographic < / > ordering. Does not cover full regex matching with =~ or integer comparison operators. |
| Related guides | Bash shell script tutorial Shell scripting course hub |
Bash compares strings with =, ==, and !=; -z and -n test whether a value is empty or non-empty, while < and > provide lexicographic ordering inside [ ] and [[ ]]. These are different from numeric operators such as -eq, -lt, and -gt, which evaluate their operands as integers — even when those operands are quoted inside [[ ]].
Bash String Comparison Quick Reference
| Goal | Typical test |
|---|---|
| Two strings equal | [[ $a == "$b" ]] or [[ $a = "$b" ]] |
| Two strings not equal | [[ $a != "$b" ]] |
| String contains substring | [[ $text == *"$needle"* ]] |
| String is empty | [[ -z "$var" ]] or [[ -z $var ]] |
| String is non-empty | [[ -n "$var" ]] or [[ -n $var ]] |
Variable is set (even to "") |
[[ -v var ]] (Bash 4.2+) |
| Filename-style suffix on the right | [[ $name == *.txt ]] (unquoted RHS) |
| Literal text on the right | [[ $name == "*.txt" ]] (quoted RHS) |
| Sort order (locale rules) | [[ "$a" < "$b" ]] |
| Case-insensitive exact match | [[ "${a,,}" == "${b,,}" ]] (Bash 4+) |
Inside [ ], quote operands so empty or multi-word values do not break the parser. Use = for equality. Bash's [ builtin also accepts escaped < and > for string ordering, but POSIX test only requires = and != for portable string comparisons — do not rely on < / > in /bin/sh scripts.
Check If Two Strings Are Equal
Inside [[ ]], quoting variable expansions is not required to prevent word splitting. For exact equality, quoting the right-hand operand is useful because an unquoted right-hand side is treated as a pattern. A common deployment check compares APP_ENV to a literal such as prod:
APP_ENV=prod
if [[ $APP_ENV == prod ]]; then
echo 'production mode'
fiproduction modeBash also accepts = as a synonym for == inside [[ ]]. Both compare the expanded left operand to the right operand:
APP_ENV=prod
if [[ $APP_ENV = prod ]]; then
echo 'match with ='
fimatch with =When the right-hand value comes from another variable and must match literally, quote it:
expected=prod
APP_ENV=prod
if [[ $APP_ENV == "$expected" ]]; then
echo 'matches expected value'
fimatches expected valueYou can quote both sides for a consistent style — [[ "$a" == "$b" ]] — but inside [[ ]] that is a readability choice, not a requirement to preserve spaces. In [ ], quote operands: [ "$APP_ENV" = prod ] is the portable form for /bin/sh scripts.
A value with spaces still compares correctly inside [[ ]] without quoting the left side:
msg='hello world'
if [[ $msg == 'hello world' ]]; then
echo 'spaces preserved'
fispaces preservedCheck If Strings Are Not Equal
Use != when you need the opposite branch:
role=admin
if [[ $role != guest ]]; then
echo 'not a guest account'
finot a guest accountThe same operator works in [ ] when the script must stay POSIX-portable — quote operands there.
Empty and Non-Empty Strings (-z, -n)
-z is true when the tested string has zero characters. -n is true when it has at least one character. Quote variable expansions in [ ]. Inside [[ ]], quoting is optional for -z and -n, though using quotes consistently is harmless.
optional=
if [[ -z $optional ]]; then
echo 'optional config is empty'
fioptional config is emptyUse -n when the next step should run only if a value is non-empty:
full=hello
if [[ -n $full ]]; then
echo 'value is present'
fivalue is presentIn [ ], always quote: [ -z "$optional" ] and [ -n "$full" ]. In practice, [[ -n "$var" ]] and [[ "$var" != "" ]] test the same thing; -z / -n read more clearly in scripts.
Empty vs Unset Variables
With the default shell options, an unset variable and a variable assigned "" both expand to an empty string. With set -u enabled, expanding an unset variable is an error.
[[ -v name ]] remains useful under set -u because it checks whether the variable exists without expanding its value:
unset maybe
maybe=
if [[ -v maybe ]]; then
echo 'maybe is set'
fimaybe is setThe same name is still empty for length tests:
maybe=
if [[ -z $maybe ]]; then
echo 'maybe is empty'
fimaybe is emptyAfter unset, the name is no longer set:
unset maybe
if [[ ! -v maybe ]]; then
echo 'maybe is unset'
fimaybe is unsetUse -v when you need to distinguish unset from set. Use -z to test whether an existing value is empty; with set -u, check -v first when the variable may be unset. [[ -v name ]] requires Bash 4.2 or newer; -z works in older shells.
Compare Strings Inside [ ] vs [[ ]]
Both forms support string equality and unary -z / -n tests. The differences matter when operands are empty, contain spaces, or use < / >.
| Topic | [ ... ] / test |
[[ ... ]] |
|---|---|---|
| Equality | [ "$a" = "$b" ] — POSIX uses = |
[[ $a = "$b" ]] or [[ $a == "$b" ]] |
| Word splitting on variables | Quote operands | Unquoted $var stays one word |
< / > ordering |
Bash: [ "$a" \< "$b" ] — not portable POSIX |
Direct: [[ "$a" < "$b" ]] |
Pattern on RHS of == |
Not supported | Unquoted RHS is a shell pattern |
POSIX [ ] guarantees = and != for string comparisons. Bash's [ builtin also supports escaped < and > for string ordering, but do not rely on those operators in portable /bin/sh scripts — Bash vs sh contrasts POSIX sh with Bash-only [[ ]] features.
A portable equality test with [ ] looks like this:
APP_ENV=prod
if [ "$APP_ENV" = prod ]; then
echo '[ ] equality works'
fi[ ] equality worksPrefer [[ ]] in Bash-only scripts when you need patterns or unquoted variables without word-splitting surprises. Prefer [ ] when /bin/sh must run the script. Bracket-level differences are summarized in Bash [ ] vs [[ ]] vs (( )).
Literal Equality vs Pattern Matching in [[ ]]
On the right side of == or =, Bash treats an unquoted operand as a shell pattern (*, ?, […]), not as a regular expression. That is useful for suffix checks:
file=report.txt
if [[ $file == *.txt ]]; then
echo 'matches .txt suffix pattern'
fimatches .txt suffix patternQuote the right-hand side when you want literal characters instead of pattern metacharacters:
file=report.txt
if [[ $file == "*.txt" ]]; then
echo 'literal match'
else
echo 'quoted RHS is literal, not a glob'
fiquoted RHS is literal, not a globreport.txt is not the same string as the five characters *.txt, so the quoted form fails even though the unquoted glob form succeeds. Use =~ when you need regular-expression features such as anchors or capture groups; for simple substring, prefix, or suffix checks, glob patterns with == are usually clearer. See Bash regex for =~ and BASH_REMATCH.
Check If a String Contains a Substring
Wrap the substring in * wildcards on both sides inside [[ ]]. Quote the variable that holds the substring while leaving the surrounding * unquoted:
text='GNU/Linux is an operating system'
needle=Linux
if [[ $text == *"$needle"* ]]; then
echo 'substring found'
fisubstring foundThe same pattern works for a fixed literal needle: [[ $text == *Linux* ]]. Use =~ only when you actually need regular-expression syntax beyond simple contains checks.
Case-Insensitive String Comparison
Default [[ ]] equality is case-sensitive. For a yes/no prompt stored as YES, lowercasing both sides is often clearer than toggling shell options:
answer=YES
if [[ "${answer,,}" == yes ]]; then
echo 'case-insensitive match'
ficase-insensitive match${var,,} is Bash variables parameter expansion and only affects the comparison you write; it does not change the stored value. ${var,,} requires Bash 4 or newer.
shopt -s nocasematch makes ==, !=, and =~ case-insensitive inside [[ ]] for the rest of the shell session (or until you turn it off):
shopt -s nocasematch
if [[ YES == yes ]]; then
echo 'nocasematch enabled'
fi
shopt -u nocasematchnocasematch enabledTurn nocasematch off when the block ends so later [[ ]] tests do not inherit case folding by accident.
Lexicographic < and > Comparison
Inside [[ ]], < and > compare strings using the active locale collation rules — not C-style integer ordering. Digit strings follow character-by-character rules, so "10" sorts before "2":
if [[ "10" < "2" ]]; then
echo 'lexical: 10 before 2'
filexical: 10 before 2That behavior is correct for text sorting and wrong for numeric magnitude. Use compare numbers in Bash when both sides are integers.
Version labels are a common trap. Lexical order compares one character at a time, so 2.10.0 sorts before 2.9.0 because the first differing characters after 2. are 1 and 9, and 1 sorts before 9:
ver_a=2.10.0
ver_b=2.9.0
if [[ $ver_a < $ver_b ]]; then
echo '2.10.0 sorts before 2.9.0 lexically'
fi2.10.0 sorts before 2.9.0 lexicallySemantic version ordering needs a numeric or dedicated version-sort tool, not raw <. When you need predictable C-locale ordering, set LC_ALL=C before the [[ ]] comparison or inside a subshell, then restore the previous locale if necessary. Otherwise assume locale settings can change sort results.
Common String-Comparison Errors
| Symptom | Likely cause | Fix |
|---|---|---|
unary operator expected |
An unquoted empty value changed the arguments passed to [ |
Quote operands |
[: too many arguments |
An unquoted value expanded into multiple words | Quote operands or use [[ ]] |
[ -n $var ] is true when var is empty |
Empty expansion disappeared, leaving [ -n ] |
Quote it: [ -n "$var" ], or use [[ -n $var ]] |
[[ "02" == "2" ]] is false |
== compares the actual text |
Use -eq or (( )) when numeric equivalence is intended |
[[ $a == $b ]] unexpectedly matches |
Unquoted RHS is a pattern | Use [[ $a == "$b" ]] for literal equality |
[[ "10" < "2" ]] is true |
Lexicographic rather than numeric ordering | Use -lt or (( )) for integers |
Yes does not equal yes |
String comparison is case-sensitive by default | Normalize case or use nocasematch |
The classic [ -n $var ] trap with an empty variable:
var=
if [ -n $var ]; then
echo '[ -n unquoted] true'
else
echo '[ -n unquoted] false'
fi[ -n unquoted] trueAfter var= expands to nothing, Bash sees [ -n ] — a one-argument test of the non-empty string -n, which succeeds. Quoting fixes it: [ -n "$var" ].
String conditions still follow normal Bash exit status rules: status 0 is true inside if. For branching structure, see Bash if, elif, else.
Summary
String comparison in Bash starts with [[ $a == "$b" ]] or [[ $a = "$b" ]] for equality, != for inequality, and -z / -n for empty and non-empty values. = works for equality in both [ ] and [[ ]]. Bash also accepts ==, but use = inside [ ] when the script must remain portable to POSIX sh. In [ ], quote operands; inside [[ ]], quote the right-hand side when you need literal matching rather than a shell pattern.
The main pitfalls are mixing string and numeric operators, forgetting that an unquoted right-hand side of == is a pattern, and using lexical < / > for version or numeric ordering. Use [[ -v name ]] when you must distinguish unset from empty, and ${var,,} when you want case-insensitive equality without leaving nocasematch on for the whole script.
For contains, prefix, and suffix checks, [[ $text == *"$needle"* ]] is usually enough. Use =~ when you need a regular expression; == is better suited to literal and shell-pattern comparisons.
References
- Bash conditional expressions — GNU Bash manual
- Bash pattern matching — glob behavior on
== - Shell parameter expansion —
${var,,}case conversion - POSIX test utility — portable
[ ]string operators
Frequently Asked Questions
What is the difference between = and == in Bash?
Inside [[ ]], = and == behave identically. With an unquoted right-hand side they perform pattern matching; quote the right-hand side for literal equality. In [ ], use = for POSIX portability.
When should I use -z versus -n?
-z is true when the string has zero characters; -n is true when it has at least one. They are opposites for practical purposes. Pick the one that matches how you phrase the condition (if empty → -z, if provided → -n).
How do I check whether a variable is unset versus empty?
[[ -v name ]] is true when the variable has been set, even to an empty string. [[ -z "$name" ]] is true when the expanded value is empty, which covers both unset and name="" unless you use set -u. Use -v when the distinction matters for defaults and optional config.
Why does quoting the right-hand side of == change matching?
Inside [[ ]], an unquoted right operand is a shell pattern, so * and ? are active. Quoting the operand disables pattern matching and compares literal text instead. That is why [[ $file == *.txt ]] and [[ $file == "*.txt" ]] behave differently.

