Compare Strings in Bash

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
Short on time?Beta Get the gist of this 11-minute read in seconds, then ask follow-up questions

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:

bash
APP_ENV=prod
if [[ $APP_ENV == prod ]]; then
  echo 'production mode'
fi
output
production mode

Bash also accepts = as a synonym for == inside [[ ]]. Both compare the expanded left operand to the right operand:

bash
APP_ENV=prod
if [[ $APP_ENV = prod ]]; then
  echo 'match with ='
fi
output
match with =

When the right-hand value comes from another variable and must match literally, quote it:

bash
expected=prod
APP_ENV=prod
if [[ $APP_ENV == "$expected" ]]; then
  echo 'matches expected value'
fi
output
matches expected value

You 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:

bash
msg='hello world'
if [[ $msg == 'hello world' ]]; then
  echo 'spaces preserved'
fi
output
spaces preserved

Check If Strings Are Not Equal

Use != when you need the opposite branch:

bash
role=admin
if [[ $role != guest ]]; then
  echo 'not a guest account'
fi
output
not a guest account

The 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.

bash
optional=
if [[ -z $optional ]]; then
  echo 'optional config is empty'
fi
output
optional config is empty

Use -n when the next step should run only if a value is non-empty:

bash
full=hello
if [[ -n $full ]]; then
  echo 'value is present'
fi
output
value is present

In [ ], 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:

bash
unset maybe
maybe=
if [[ -v maybe ]]; then
  echo 'maybe is set'
fi
output
maybe is set

The same name is still empty for length tests:

bash
maybe=
if [[ -z $maybe ]]; then
  echo 'maybe is empty'
fi
output
maybe is empty

After unset, the name is no longer set:

bash
unset maybe
if [[ ! -v maybe ]]; then
  echo 'maybe is unset'
fi
output
maybe is unset

Use -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:

bash
APP_ENV=prod
if [ "$APP_ENV" = prod ]; then
  echo '[ ] equality works'
fi
output
[ ] equality works

Prefer [[ ]] 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:

bash
file=report.txt
if [[ $file == *.txt ]]; then
  echo 'matches .txt suffix pattern'
fi
output
matches .txt suffix pattern

Quote the right-hand side when you want literal characters instead of pattern metacharacters:

bash
file=report.txt
if [[ $file == "*.txt" ]]; then
  echo 'literal match'
else
  echo 'quoted RHS is literal, not a glob'
fi
output
quoted RHS is literal, not a glob

report.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:

bash
text='GNU/Linux is an operating system'
needle=Linux

if [[ $text == *"$needle"* ]]; then
  echo 'substring found'
fi
output
substring found

The 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:

bash
answer=YES
if [[ "${answer,,}" == yes ]]; then
  echo 'case-insensitive match'
fi
output
case-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):

bash
shopt -s nocasematch
if [[ YES == yes ]]; then
  echo 'nocasematch enabled'
fi
shopt -u nocasematch
output
nocasematch enabled

Turn 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":

bash
if [[ "10" < "2" ]]; then
  echo 'lexical: 10 before 2'
fi
output
lexical: 10 before 2

That 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:

bash
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'
fi
output
2.10.0 sorts before 2.9.0 lexically

Semantic 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:

bash
var=
if [ -n $var ]; then
  echo '[ -n unquoted] true'
else
  echo '[ -n unquoted] false'
fi
output
[ -n unquoted] true

After 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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)