Linux & Bash · Part I · Module 16 · Conditionals and Pattern Branches
String and Numeric Comparisons
Bash will compare ten and nine and tell you that ten is smaller. It is not a bug and there is no warning — the operator you chose decides whether you are comparing numbers or comparing characters, and a script that mixes them up gives wrong answers for years without anyone noticing.
Intermediate22 min readLinuxPart IBashConditionalsComparisonsScripting
Part I · Module 16 · Chapter 16.3 · String and Numeric Comparisons
Chapter 16.2 ended on a test that reported ten as smaller than nine. That was not an artefact of the brackets. It is the central fact of this chapter, and it deserves its own page.
1. The Same Two Values, Two Answers
$ [ 10 -gt 9 ] && echo "greater" || echo "not greater"
greater
$ [ 10 \> 9 ] && echo "greater" || echo "not greater"
not greaterIdentical operands. Opposite answers. No error, no warning, no hint that anything unusual happened in either case.
-gt asks a numeric question: is the number ten larger than the number nine? Yes.
> asks a text question: does the text 10 sort after the text 9? It compares character by character, finds 1 against 9 at the very first position, and 1 comes first. So no.
Why the operator has to match the kind of value
The operands carry no type. Everything in the shell is text — Chapter 8.1 established that, and this is where it bites. 10 is not a number that Bash happens to print as text; it is the two characters 1 and 0, and the only thing that decides how they are interpreted is the operator standing between them.
2. The Two Families
Numeric, all spelled as words, all integers only:
| Operator | True when | 5 vs 10 |
|---|---|---|
-eq | equal | false |
-ne | not equal | true |
-lt | less than | true |
-le | less than or equal | true |
-gt | greater than | false |
-ge | greater than or equal | false |
String, all spelled as symbols:
| Operator | True when |
|---|---|
= | identical text |
!= | different text |
\< | sorts before (escaped in [ ]) |
\> | sorts after (escaped in [ ]) |
-z | the text is empty |
-n | the text is not empty |
The mnemonic is the whole trick, and it is worth memorising because it has no exceptions: words for numbers, symbols for strings. -gt looks like an abbreviation of "greater than" and compares numbers; > looks like a maths symbol and compares text. The spellings feel backwards, which is exactly why people mix them up.
$ [ "PASS" = "PASS" ] && echo "same"
same
$ [ "PASS" != "FAIL" ] && echo "different"
different
$ [ -z "" ] && echo "empty"
empty
$ [ -n "x" ] && echo "not empty"
not empty3. Where the Mismatch Actually Hurts
Comparing literals is a classroom exercise. Real scripts compare values that arrived from somewhere, and that is where the two families diverge in ways you cannot see.
Leading zeros. A run number pulled from a filename or a log:
$ [ 01 -eq 1 ] && echo "equal" || echo "not equal"
equal
$ [ 01 = 1 ] && echo "equal" || echo "not equal"
not equalNumerically identical, textually different. If your test IDs are test_001 through test_050 and you extract the number, -eq works and = does not.
Surrounding whitespace. This one is genuinely useful:
$ [ " 5" -eq 5 ] && echo "equal" || echo "not equal"
equal
$ [ " 5" = "5" ] && echo "equal" || echo "not equal"
not equalNumeric comparison tolerates the space; string comparison cannot. A value sliced out of a column with cut or awk very often carries a leading space (Module 13), and -eq quietly absorbs it while = reports a mismatch that looks inexplicable until you pipe the value through cat -A.
Error reporting. The arithmetic family refuses nonsense, and that refusal is worth something:
$ [ abc -eq 1 ]
bash: [: abc: integer expression expected
$ echo $?
2
$ [ abc = 1 ]; echo $?
1The numeric comparison told you the value was not a number — status 2, which Chapter 11.1 taught you means "something went wrong" rather than "the answer is no". The string comparison just said no. When a script is parsing a log and the field it wanted turns out to be a word, the version that complains is the version you can debug.
Decimals do not exist. Bash has no floating-point arithmetic at all:
$ [ 1.5 -gt 1 ]
bash: [: 1.5: integer expression expectedFor a coverage percentage, a runtime in seconds, or anything else with a decimal point, you need an external tool:
$ cov="94.7"
$ awk -v c="$cov" 'BEGIN { exit !(c >= 90) }' && echo "coverage goal met"
coverage goal metThat is awk from Chapter 13.7 being used purely for its exit status — the BEGIN block runs, computes the comparison and exits 0 or 1, and nothing is printed. bc -l is the other common choice. Both are external processes, so neither belongs inside a loop running ten thousand times, but for one comparison per run they are the right answer.
Version numbers never compare as text. A trap worth seeing once:
$ [[ "2.10" > "2.9" ]] && echo "newer" || echo "older"
olderVersion 2.10 is newer than 2.9 and the string comparison says otherwise, for the same first-differing-character reason as 10 and 9. There is no operator in [ ] or [[ ]] that compares versions. sort -V (Chapter 13.3) does, and comparing two versions correctly in Bash means sorting them and looking at which came out first — more than a single test, and worth knowing before you write one.
4. The Portable Spelling of Equality
= is the POSIX string-equality operator. == also works in Bash:
$ [ "a" == "a" ] && echo "works"
worksBut that is Bash's builtin being generous, and it does not travel:
$ dash -c '[ a == a ]'
dash: 1: [: a: unexpected operatorSo: inside [ ], write =. Inside [[ ]], write == — it is the form that enables glob matching (Chapter 16.2), = is accepted there as a synonym, and [[ ]] is already non-portable so there is nothing further to lose.
5. Arithmetic Conditions, and a Zero That Means the Opposite
There is a third way to compare numbers, and it is the nicest to read:
$ n=10
$ (( n > 9 )) && echo "greater"
greaterInside (( )) you write ordinary arithmetic — Chapter 8.5's arithmetic context, now used as a condition. The operators are the ones from every other language, no escaping is needed, and $ on the variables is optional because the whole thing is an arithmetic expression already.
But (( )) is not a test. It evaluates an expression and then reports a status based on the value, and the rule is inverted from everything else in this module:
$ (( 0 )); echo $?
1
$ (( 1 )); echo $?
0(( 0 )) is false. Compare that with Chapter 16.1, where [ 0 ] was true. They are not inconsistent — they are two different mechanisms. [ 0 ] asks whether a string is non-empty and converts the answer to a status. (( 0 )) computes a number and then applies the C convention, where zero is false, converting that to a status. Two conventions about zero, in the same language, three characters apart.
This makes (( )) the one place in Bash where a C-style truth test works as you expect:
$ errors=0
$ (( errors )) && echo "there were errors" || echo "clean"
clean
$ errors=3
$ (( errors )) && echo "there were errors" || echo "clean"
there were errorsThat is idiomatic and readable. It is also the only Bash construct where writing if [ $count ] — the Chapter 16.1 bug — has a correct equivalent that does what the author meant.
6. Two Sharp Edges on (( ))
A leading zero means octal. This one is not theoretical:
$ (( 08 > 5 ))
bash: ((: 08: value too great for base (error token is "08")
$ (( 010 == 10 )) && echo "equal" || echo "not equal"
not equal
$ (( 010 == 8 )) && echo "it is octal"
it is octal010 is eight. 08 is not a valid number at all, because base 8 has no digit 8. And here is the asymmetry that makes this worth a section rather than a footnote:
$ [ 010 -eq 10 ] && echo "test says equal"
test says equaltest reads 010 as ten. Arithmetic reads it as eight. The same characters, two different numbers, depending on which comparison you used. A script extracting a zero-padded run number — run_008, run_010 — gives one answer with [ -eq ] and either a different answer or a hard error with (( )).
The fix is to state the base explicitly:
$ n=08
$ (( 10#$n > 5 )) && echo "decimal, and greater"
decimal, and greater10# is Chapter 8.5's base prefix, and it is the right habit for any number that came out of a filename, a log field or a date.
It swallows non-numbers silently. The opposite failing from [ ]:
$ n=""
$ (( n > 5 )); echo $?
1
$ n="abc"
$ (( n > 5 )); echo $?
1No error either time. In an arithmetic context an empty or non-numeric name evaluates to 0, so both became 0 > 5, which is false. [ "$n" -gt 5 ] would have told you the value was unusable; (( n > 5 )) reports a confident, wrong-shaped "no".
7. Comparing an Exit Status
The commonest numeric comparison in real scripts is on $?, because it is how you separate "it said no" from "it broke" — the distinction Chapter 16.1's else branch could not make:
#!/usr/bin/env bash
# scan.sh — classify a log, distinguishing failure from absence
# usage: scan.sh <pattern> <logfile>
grep -q "$1" "$2"
rc=$?
if [ "$rc" -eq 0 ]; then
echo "found"
elif [ "$rc" -eq 1 ]; then
echo "not found"
else
echo "scan.sh: could not read $2 (grep status $rc)" >&2
exit "$rc"
fi$ scan.sh root /etc/passwd
found
$ scan.sh zzzzz /etc/passwd
not found
$ scan.sh root /no/such/file
grep: /no/such/file: No such file or directory
scan.sh: could not read /no/such/file (grep status 2)Two messages, because grep reported the problem itself before the script added its own line. In a wrapper you would silence grep with 2>/dev/null and keep only your message — but only once you are sure your message says everything grep's did.
rc=$? is the line immediately after grep, for Chapter 11.1's reason. And "$rc" is quoted even though it is certain to be a number, because the habit should not have exceptions — Chapter 16.2 showed what an unquoted empty value does to [ ].
8. A Complete Script
A regression gate, using both families where each belongs:
#!/usr/bin/env bash
# gate.sh — decide whether a regression result is acceptable
# usage: gate.sh <status-word> <error-count> <coverage-percent>
set -o nounset
verdict=$1
errors=$2
coverage=$3
# string family: a word is a word
if [ "$verdict" != "COMPLETED" ]; then
echo "GATE FAIL run did not complete (verdict: $verdict)"
exit 1
fi
# numeric family: 10# because the count may be zero-padded
if [ "$(( 10#$errors ))" -gt 0 ]; then
echo "GATE FAIL $(( 10#$errors )) errors"
exit 1
fi
# awk: coverage has a decimal point, so Bash cannot compare it
if ! awk -v c="$coverage" 'BEGIN { exit !(c >= 90) }'; then
echo "GATE FAIL coverage $coverage% is below 90%"
exit 1
fi
echo "GATE PASS no errors, coverage $coverage%"$ ./gate.sh COMPLETED 000 94.7
GATE PASS no errors, coverage 94.7%
$ ./gate.sh COMPLETED 002 94.7
GATE FAIL 2 errors
$ ./gate.sh KILLED 000 94.7
GATE FAIL run did not complete (verdict: KILLED)
$ ./gate.sh COMPLETED 000 88.2
GATE FAIL coverage 88.2% is below 90%Three comparisons, three different mechanisms, each chosen because of what its operand is: a word compared with !=, a zero-padded integer compared with -gt after 10#, and a decimal handed to awk because Bash genuinely cannot do it.
Practice
- Run all four of
[ 10 -gt 9 ],[ 10 \> 9 ],[[ 10 -gt 9 ]]and[[ 10 > 9 ]], printing$?each time. Explain the pattern in one sentence. - Predict then measure:
[ 007 -eq 7 ],[ 007 = 7 ],(( 007 == 7 )). One of the three is a surprise — which, and why? - Run
(( 09 ))and(( 9 )). Explain the error from the first, then make it work without editing the digits. - Set
v=" 12 "with spaces on both sides. Which of[ "$v" -eq 12 ]and[ "$v" = "12" ]succeeds? Useprintf '%q\n' "$v"to see exactly what you have. - Set
n="". Compare the output and status of[ "$n" -gt 5 ]against(( n > 5 )). Which failure mode would you rather ship in a log parser? - Write a test that is true when
$covis at least 90, wherecov="89.99". Try it with[ ]first and say exactly why it cannot be done, then do it withawk. - Using
sort -V, write a functionnewer_than A Bthat succeeds when version A is at least B. Check it on2.10against2.9, where[[ > ]]gets the wrong answer. - Take
gate.shand remove the10#. Find an input that changes the verdict, and one that makes it error out.
Check Your Understanding
- Why is
[ 10 \> 9 ]false? - What is the mnemonic for which operators are numeric and which are string?
[ 01 = 1 ]is false but[ 01 -eq 1 ]is true. Which would you use for a run number extracted fromrun_01.log?[ 0 ]is true but(( 0 ))is false. Are these inconsistent?(( 010 == 10 ))is false. What is010, and why does[ 010 -eq 10 ]disagree?- How do you compare
94.7against90in Bash? [ "$n" -gt 5 ]errors whenn="abc";(( n > 5 ))returns false. Which is more useful, and when?
9. Remember
- The operator decides the kind of comparison, not the values. Everything in the shell is text, so there is no type to infer.
- Words for numbers, symbols for strings:
-eq -ne -lt -le -gt -gecompare numerically;= != \< \> -z -ncompare text. - The wrong operator is silent.
[ 10 \> 9 ]is false, with no error. -zand-nask only whether there is any text.[ -z "0" ]is false.- Numeric comparison tolerates surrounding whitespace and leading zeros; string comparison does not.
[ ]reports a non-numeric operand (status 2);(( ))silently treats it as zero. Pick the failure mode you want.(( 0 ))is false while[ 0 ]is true — two different conventions about zero, three characters apart.- A leading zero is octal inside
(( ))—010is eight and08is an error — buttestreads it as decimal. Use10#$n. - Write
=inside[ ]and==inside[[ ]].==in[ ]works in Bash and fails indash. - Bash has no decimals and no version comparison. Use
awkorbc -lfor the first andsort -Vfor the second. - String ordering depends on the locale.
=and!=do not.
Chapter 16.4 moves from comparing values to asking about files — whether one exists, what type it is, whether it is empty, and whether you can actually read it. It is the most common guard in real scripts and the cheapest way to turn a crash into a message.
Where this fits
Part of the Linux & Bash curriculum.
