Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ 10 -gt 9 ] && echo "greater" || echo "not greater"
greater

$ [ 10 \> 9 ] && echo "greater" || echo "not greater"
not greater

Identical 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

Why the comparison operator has to match the kind of value. Comparing ten and nine with the numeric operator gives the answer a person expects. Comparing the same two values with the string operator compares their first characters, so ten sorts before nine and the test is false — with no error to indicate anything went wrong.Ten against ninecompared with the numericoperatorTheir values arecomparedas the numbers they representTen is the largerwhich is the answer you wantedThe same two valuescompared with the stringoperatorTheir characters arecomparedand the first characterdecides itTen sorts first, so thetest is falseand nothing reports a problem12
Both comparisons receive the same two operands. The numeric operator compares them as the quantities they represent and gives the answer a person expects. The string operator compares their characters, decides on the very first one, and reports false — correctly, by its own rules, and with nothing at all to indicate that the question asked was not the question intended.

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:

OperatorTrue when5 vs 10
-eqequalfalse
-nenot equaltrue
-ltless thantrue
-leless than or equaltrue
-gtgreater thanfalse
-gegreater than or equalfalse

String, all spelled as symbols:

OperatorTrue when
=identical text
!=different text
\<sorts before (escaped in [ ])
\>sorts after (escaped in [ ])
-zthe text is empty
-nthe 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ "PASS" = "PASS" ]  && echo "same"
same
$ [ "PASS" != "FAIL" ] && echo "different"
different
$ [ -z "" ]            && echo "empty"
empty
$ [ -n "x" ]           && echo "not empty"
not empty

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ 01 -eq 1 ] && echo "equal"     || echo "not equal"
equal
$ [ 01 = 1 ]   && echo "equal"     || echo "not equal"
not equal

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ " 5" -eq 5 ]  && echo "equal" || echo "not equal"
equal
$ [ " 5" = "5" ]  && echo "equal" || echo "not equal"
not equal

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ abc -eq 1 ]
bash: [: abc: integer expression expected
$ echo $?
2

$ [ abc = 1 ]; echo $?
1

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ 1.5 -gt 1 ]
bash: [: 1.5: integer expression expected

For a coverage percentage, a runtime in seconds, or anything else with a decimal point, you need an external tool:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cov="94.7"
$ awk -v c="$cov" 'BEGIN { exit !(c >= 90) }' && echo "coverage goal met"
coverage goal met

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [[ "2.10" > "2.9" ]] && echo "newer" || echo "older"
older

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ "a" == "a" ] && echo "works"
works

But that is Bash's builtin being generous, and it does not travel:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ dash -c '[ a == a ]'
dash: 1: [: a: unexpected operator

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ n=10
$ (( n > 9 )) && echo "greater"
greater

Inside (( )) 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ (( 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ errors=0
$ (( errors )) && echo "there were errors" || echo "clean"
clean
$ errors=3
$ (( errors )) && echo "there were errors" || echo "clean"
there were errors

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ (( 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 octal

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ [ 010 -eq 10 ] && echo "test says equal"
test says equal

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ n=08
$ (( 10#$n > 5 )) && echo "decimal, and greater"
decimal, and greater

10# 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 [ ]:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ n=""
$ (( n > 5 )); echo $?
1
$ n="abc"
$ (( n > 5 )); echo $?
1

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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%"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./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

  1. Run all four of [ 10 -gt 9 ], [ 10 \> 9 ], [[ 10 -gt 9 ]] and [[ 10 > 9 ]], printing $? each time. Explain the pattern in one sentence.
  2. Predict then measure: [ 007 -eq 7 ], [ 007 = 7 ], (( 007 == 7 )). One of the three is a surprise — which, and why?
  3. Run (( 09 )) and (( 9 )). Explain the error from the first, then make it work without editing the digits.
  4. Set v=" 12 " with spaces on both sides. Which of [ "$v" -eq 12 ] and [ "$v" = "12" ] succeeds? Use printf '%q\n' "$v" to see exactly what you have.
  5. 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?
  6. Write a test that is true when $cov is at least 90, where cov="89.99". Try it with [ ] first and say exactly why it cannot be done, then do it with awk.
  7. Using sort -V, write a function newer_than A B that succeeds when version A is at least B. Check it on 2.10 against 2.9, where [[ > ]] gets the wrong answer.
  8. Take gate.sh and remove the 10#. Find an input that changes the verdict, and one that makes it error out.

Check Your Understanding

  1. Why is [ 10 \> 9 ] false?
  2. What is the mnemonic for which operators are numeric and which are string?
  3. [ 01 = 1 ] is false but [ 01 -eq 1 ] is true. Which would you use for a run number extracted from run_01.log?
  4. [ 0 ] is true but (( 0 )) is false. Are these inconsistent?
  5. (( 010 == 10 )) is false. What is 010, and why does [ 010 -eq 10 ] disagree?
  6. How do you compare 94.7 against 90 in Bash?
  7. [ "$n" -gt 5 ] errors when n="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 -ge compare numerically; = != \< \> -z -n compare text.
  • The wrong operator is silent. [ 10 \> 9 ] is false, with no error.
  • -z and -n ask 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 (( )) — 010 is eight and 08 is an error — but test reads it as decimal. Use 10#$n.
  • Write = inside [ ] and == inside [[ ]]. == in [ ] works in Bash and fails in dash.
  • Bash has no decimals and no version comparison. Use awk or bc -l for the first and sort -V for 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.