0

String to floating comparison not working in shell script

REQUIRED_VERSION=2.38.0
VERSION=$(echo $FULL_DATA | cut -d ":" -f 5)   #This gives values as 2.10.0 OR 2.40

if [ $VERSION -gt $REQUIRED_VERSION ]]
  then
    echo "===  PASSED ==== "
  else
    echo "=== FAILED ==="
    #exit 1
fi

Getting error as below with zsh

test.sh:60: bad floating point constant

It always gives results as passed with sh

I also tried bc but won't work, maybe missing syntax not sure about it.

Please suggest some way to typecast from string to float which I can compare further with shell or zsh

Shubham Jain
  • 16,610
  • 15
  • 78
  • 125
  • `bash` only supports integer math. And `3.28.0` is going to give issues with tools that do have floating point numbers. – Shawn Jul 26 '23 at 10:12
  • Did you consider switching to a shell which can actually do floats (zsh for instance)? – user1934428 Jul 26 '23 at 10:19
  • @Shawn - thanks for response, yes I have read it somewhere too, do you think cut this in floating to num and compare would be the good idea? – Shubham Jain Jul 26 '23 at 10:25
  • @user1934428 - I haven't try this .. does it support floating? do we have any method to convert string to float ... could you share one exmaple – Shubham Jain Jul 26 '23 at 10:27
  • In zsh, numeric context includes floating point arithmetic. You just have to use `[[ ... ]]` instead ofn`[`. Since zsh has quite some similarities to bash, it is easy to learn. – user1934428 Jul 26 '23 at 10:31
  • could you add above example as answer here with zsh .. it will help – Shubham Jain Jul 26 '23 at 10:32
  • 1
    Version strings are not floats. How should 1.2.3 compare to 3.5.1. Gets even worse with additions like beta or rc4 or versions like "5.18.16-1~bpo11+1". https://stackoverflow.com/a/4024263/8165349 has some answers to compare versions in Bash. I would build something around `sort -V`. – Paul Pazderski Jul 26 '23 at 11:59
  • In zsh, you would write the test as `[[ $VERSION -gt $REQUIRED_VERSION ]]`. Or, if you preferr: `(( VERSION > REQUIRED_VERSION ))`. – user1934428 Jul 26 '23 at 12:39
  • Get out of the habit of using ALLCAPS variable names, leave those as reserved by the shell. One day you'll write `PATH=something` and then [wonder](https://stackoverflow.com/q/27555060/7552) [why](https://stackoverflow.com/q/28310594/7552). your script is [broken](https://unix.stackexchange.com/q/114596/4667) – glenn jackman Jul 26 '23 at 13:20

4 Answers4

3

Version numbers are not mathematical numbers. Bash does not have a built-in way to compare version numbers. The sort command has -V for comparing version numbers and -C for reporting whether a file is correctly sorted. You can use this with Bash‘s <<< feature to pass immediate input to a command:

#!/bin/bash -e

A="2.38.0"
B="2.3.1"

if sort -C -V <<<"$A"$'\n'"$B"; then
    echo "A is earlier than or the same as B."
else
    echo "A is later than B."
fi

The $'\n' syntax is a Bash feature for producing a new-line character.

Bash allows you to negate the test with a ! before the command:

if ! sort -C -V <<<"$A"$'\n'"$B"; then

Note that sort will report “2.38” is earlier than “2.38.0”.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
2

You can't compare versions as any kind of number, including floats, nor as strings as each .-separated section individually needs to be compared numerically. You need to compare them specifically as versions, e.g. using GNU sort for -V to sort a list of version numbers into lowest to highest order:

$ cat tst.sh
#!/usr/bin/env bash

required_version=2.38.0
actual_version=10.7.45

highest_version=$(printf '%s\n%s\n' "$required_version" "$actual_version" | sort -V | tail -1)

if [[ $highest_version == $actual_version ]]; then
    echo "=== PASSED ==="
else
    echo "=== FAILED ==="
fi

$ ./tst.sh
=== PASSED ===

If you don't have GNU sort you could always write your own comparison function, e.g.:

$ cat tst.sh
#!/usr/bin/env bash

required_version=2.38.0
actual_version=10.7.45

compare_vers() {
    local a b n

    IFS='.' read -r -a a <<< "$1"
    IFS='.' read -r -a b <<< "$2"

    (( "${#a[@]}" > "${#b[@]}" )) && n="${#a[@]}" || n="${#b[@]}"

    for (( i=0; i<n; i++ )); do
        if (( "${a[i]:0}" > "${b[i]:0}" )); then
            echo 'BIGGER'
            return
        elif (( "${a[i]:0}" < "${b[i]:0}" )); then
            echo 'SMALLER'
            return
        fi
    done

    echo 'EQUAL'
}

rslt=$(compare_vers "$actual_version" "$required_version")

if [[ $rslt == "BIGGER" ]]; then
    echo "=== PASSED ==="
else
    echo "=== FAILED ==="
fi

$ ./tst.sh
=== PASSED ===
Ed Morton
  • 188,023
  • 17
  • 78
  • 185
  • thats work ED .. thank you .. one more small doubt , how to do greater than and equal to in if statement ... for me if [[ $highest_version > $actual_version ]]; works but not >= – Shubham Jain Jul 26 '23 at 12:30
  • i am trying this ->. if [[ $highest_version>$actual_version && $highest_version=$actual_version ]]; -> it fails when they are equal too, any idea why? – Shubham Jain Jul 26 '23 at 12:42
  • With `>` or `>=` you're still trying to compare versions as either a string or a number. As I mentioned, you can't do that as a version is not a number (unless it only contains 1 `.`) so versions can't be compared numerically and they can't be compared alphabetically character by character as happens in string comparison. If you tell me what you want to do (rather than showing me how you're trying to do it), I can help. – Ed Morton Jul 26 '23 at 13:26
1

This can be done in pure bash, but of course it's more code:

required_version=2.38.0

ver2float() {
    local parts part frac=""
    IFS=. read -ra parts <<<"$1"
    for part in "${parts[@]:1}"; do
        frac+=$(printf "%03d" "$part")
    done
    printf "%s.%s" "${parts[0]}" "${frac:-0}"
}

compare_floats() {
    (( 10#${1%.*}  > 10#${2%.*} )) && return 1
    (( 10#${1%.*} <  10#${2%.*} )) && return 0
    (( 10#${1#*.} <= 10#${2#*.} ))
}

for version in  1.0  2.37.99  2.38.0  2.38.1  12; do
    if compare_floats "$(ver2float "$required_version")" \
                      "$(ver2float "$version")" 
    then
        echo "$required_version is less than or equal to $version"
    else
        echo "$required_version is greater than $version"
    fi
done

This outputs

2.38.0 is greater than 1.0
2.38.0 is greater than 2.37.99
2.38.0 is less than or equal to 2.38.0
2.38.0 is less than or equal to 2.38.1
2.38.0 is less than or equal to 12
glenn jackman
  • 238,783
  • 38
  • 220
  • 352
1

zsh ships with a function for comparing version numbers:

autoload is-at-least
is-a-least 2.38 2.0
print $?
# => 1

It can accept dot or dash as the separator, and it looks like it can handle letters as well:

#!/usr/bin/env zsh
autoload is-at-least
tstVer(){
  local m=nope
  is-at-least ${1:?} ${2:?} && m=WORKS
  printf 'need: %-6s have: %-6s %s\n' $1 $2 $m
}
tstVer 0.3 1.0
tstVer 3 4.3-33
tstVer 4.4 4.3-33
tstVer 2.3d 2.3c
tstVer 5.3.0 5.3
tstVer 5.3 5.3.0

# => need: 0.3    have: 1.0    WORKS
# => need: 3      have: 4.3-33 WORKS
# => need: 4.4    have: 4.3-33 nope
# => need: 2.3d   have: 2.3c   nope
# => need: 5.3.0  have: 5.3    WORKS
# => need: 5.3    have: 5.3.0  WORKS
Gairfowl
  • 2,226
  • 6
  • 9