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 command / 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 command 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:

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

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

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

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

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

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

output
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 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)