Bash compare strings: equality, order, empty checks, and pattern match

Bash string comparison with == and !=, bash if string equals patterns, compare strings in bash using [[ ]] and [ ], ASCII order, empty (-z) and non-empty (-n) checks, quoting, case folding, and =~ regex.

Published

Updated

Read time 5 min read

Reviewed byDeepak Prasad

Bash compare strings: equality, order, empty checks, and pattern match

Scripts spend a lot of time comparing text: two paths must match, a flag must read yes, or a variable from grep / pidof might be empty. That is bash string comparison—not the same as integer comparison: anything wrapped in quotes is a string, and -eq / -gt belong to arithmetic tests. The sections below walk through equality, order, empty checks, and a small =~ regex example, with quoting called out because that is where most bugs hide.

Tested all the commands and code from this article on Ubuntu 25.04, kernel 6.14.0-37-generic, Bash 5.2.37.


Strings vs integers (quick sanity)

Text in single or double quotes is a string, even when the characters look numeric ("10" is not the integer ten for -eq purposes). Unquoted 10 can behave like an arithmetic value in some contexts. If you need to treat user input as a number, say so explicitly—see check if string contains numbers before mixing models.


Operator cheat sheet (string comparison bash)

Intent [ ... ] / test [[ ... ]]
equal [ "$a" = "$b" ] [[ "$a" == "$b" ]] or [[ "$a" = "$b" ]]
not equal [ "$a" != "$b" ] [[ "$a" != "$b" ]]
less than (locale / collation) [ "$a" \< "$b" ] [[ "$a" < "$b" ]]
greater than [ "$a" \> "$b" ] [[ "$a" > "$b" ]]
zero length [ -z "$a" ] [[ -z "$a" ]]
non-empty [ -n "$a" ] [[ -n "$a" ]]

Inside [[, < and > are string comparisons, not redirection. Inside [, you must escape them as \< and \> (or use separate tests with sort / cmp if you prefer to avoid escapes).


Bash if string equals (and not equals)

Use quotes on both sides so spaces and empty values behave:

bash
VAR1="golinuxcloud"
VAR2="golinuxcloud"

if [[ "$VAR1" == "$VAR2" ]]; then
  echo "exit status: $?"
  echo "$VAR1 is equal to $VAR2"
else
  echo "exit status: $?"
  echo "$VAR1 is NOT equal to $VAR2"
fi

Output:

text
exit status: 0
golinuxcloud is equal to golinuxcloud
bash
VAR1="golinuxcloud"
VAR2="website"

if [[ "$VAR1" != "$VAR2" ]]; then
  echo "exit status: $?"
  echo "$VAR1 is NOT equal to $VAR2"
fi

Output:

text
exit status: 0
golinuxcloud is NOT equal to website

= and == are equivalent for string equality inside [[ in Bash; in [, use a single = (POSIX).


Ordering: bash compare string with < / >

Lexicographic order follows your locale’s collation rules, not always raw ASCII. A simple A vs B check:

bash
VAR1="A"
VAR2="B"

if [[ "$VAR1" < "$VAR2" ]]; then
  echo "exit status: $?"
  echo "$VAR1 sorts before $VAR2"
elif [[ "$VAR1" == "$VAR2" ]]; then
  echo "equal"
else
  echo "exit status: $?"
  echo "$VAR1 sorts after $VAR2"
fi

Output here:

text
exit status: 0
A sorts before B

That matches the usual code point order for these two letters in the C.UTF-8 style locale on this machine; if you need strict byte order regardless of locale, set LC_ALL=C for the test or for the whole script while comparing.


Empty vs non-empty (-z and -n)

-z is true when the expansion is empty; -n is the opposite. Always quote:

bash
VAR=$(pidof nonexistentprocess12345 2>/dev/null)

if [ -z "$VAR" ]; then
  echo "exit status: $?"
  echo "VAR is empty"
fi

Output:

text
exit status: 0
VAR is empty

Non-empty check:

bash
VAR="hello"

if [ -n "$VAR" ]; then
  echo "exit status: $?"
  echo "VAR has content"
fi

Output:

text
exit status: 0
VAR has content

Case sensitivity and nocasematch

By default [[ "$a" == "$b" ]] is case sensitive. For case-insensitive equality:

bash
shopt -s nocasematch
[[ "Hello" == "hello" ]] && echo "equal ignoring case"
shopt -u nocasematch

Output:

text
equal ignoring case

Turn nocasematch off when you are done so the rest of the script does not inherit surprises.


Substring and glob-style checks (without =~)

== compares the whole string. To match a substring with globs, build the right-hand side as: *, then a quoted literal ("deepak"), then * again.

bash
[[ "my name is deepak prasad" == *"deepak"* ]] && echo "substring match"

Output:

text
substring match

A literal == "deepak" against the full sentence fails, which is why people reach for globs or regex.


Regex match with =~

[[ string =~ regex ]] uses extended regex on the right. Quoting parts of the pattern turns those pieces literal, which is useful when you need a fixed substring inside a larger pattern—otherwise keep the pattern unquoted so metacharacters work.

Examples:

bash
[[ "my name is deepak prasad" =~ prasad$ ]] && echo "bash regex match"
[[ "my name is deepak prasad" =~ ^my ]] && echo "starts with my"

Output:

text
bash regex match
starts with my

If the match fails, the status is non-zero—handy for if [[ ... =~ ... ]]; then.


Practical habits

  • Quote every variable unless you mean to split or glob.
  • Prefer [[ in Bash-only scripts; keep [ when /bin/sh must run the script.
  • Do not confuse == (string) with -eq (integer)—that is a common source of “bash string comparison looks broken” reports.
  • Remember collation: sorting Z vs a depends on LC_ALL / LC_COLLATE.

Summary

For most day-to-day work, quoted [[ "$a" == "$b" ]] is your bash if string equals check, != covers inequality, < / > handle sort order once you know what locale you are in, and -z / -n answer “empty pipe output or not?”. Turn on nocasematch briefly when case should not matter; use == with a glob on the right or =~ when you need a pattern instead of a full-string match. Keep arithmetic tests in their own mental bucket so you never mix -eq with raw text again.

Further reading:

Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …