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:
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"
fiOutput:
exit status: 0
golinuxcloud is equal to golinuxcloudVAR1="golinuxcloud"
VAR2="website"
if [[ "$VAR1" != "$VAR2" ]]; then
echo "exit status: $?"
echo "$VAR1 is NOT equal to $VAR2"
fiOutput:
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:
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"
fiOutput here:
exit status: 0
A sorts before BThat 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:
VAR=$(pidof nonexistentprocess12345 2>/dev/null)
if [ -z "$VAR" ]; then
echo "exit status: $?"
echo "VAR is empty"
fiOutput:
exit status: 0
VAR is emptyNon-empty check:
VAR="hello"
if [ -n "$VAR" ]; then
echo "exit status: $?"
echo "VAR has content"
fiOutput:
exit status: 0
VAR has contentCase sensitivity and nocasematch
By default [[ "$a" == "$b" ]] is case sensitive. For case-insensitive equality:
shopt -s nocasematch
[[ "Hello" == "hello" ]] && echo "equal ignoring case"
shopt -u nocasematchOutput:
equal ignoring caseTurn 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.
[[ "my name is deepak prasad" == *"deepak"* ]] && echo "substring match"Output:
substring matchA 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:
[[ "my name is deepak prasad" =~ prasad$ ]] && echo "bash regex match"
[[ "my name is deepak prasad" =~ ^my ]] && echo "starts with my"Output:
bash regex match
starts with myIf 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/shmust 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
Zvsadepends onLC_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:

