9

Format of versions - X.X.X.X.
Where X - number.
What is the best way to compare two versions?
I use following code:

compareVersions()
{
  VER_1=$1
  VER_2=$2

  print -R "$VER_1"| IFS=. read v1_1 v1_2 v1_3 v1_4
  print -R "$VER_2"| IFS=. read v2_1 v2_2 v2_3 v2_4

  RESULT="0"

  if [[ "${v1_1}" -lt "${v2_1}" ]]
  then
     RESULT="-1"
  elif [[ "${v1_1}" -gt "${v2_1}" ]]
  then
     RESULT="1"
  elif [[ "${v1_2}" -lt "${v2_2}" ]]
  then
     RESULT="-1"
  elif [[ "${v1_2}" -gt "${v2_2}" ]]
  then
     RESULT="1"
  elif [[ "${v1_3}" -lt "${v2_3}" ]]
  then
     RESULT="-1"
  elif [[ "${v1_3}" -gt "${v2_3}" ]]
  then
     RESULT="1"
  elif [[ "${v1_4}" -lt "${v2_4}" ]]
  then
     RESULT="-1"
  elif [[ "${v1_4}" -gt "${v2_4}" ]]
  then
     RESULT="1"
  fi

  echo "$RESULT"
}

But I do not like it - it is very straightforward.
Maybe is there much correct way to compare versions?

Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
Volodymyr Bezuglyy
  • 16,295
  • 33
  • 103
  • 133

6 Answers6

13

Pure Bash / Ksh:

compareVersions ()
{
  typeset    IFS='.'
  typeset -a v1=( $1 )
  typeset -a v2=( $2 )
  typeset    n diff

  for (( n=0; n<4; n+=1 )); do
    diff=$((v1[n]-v2[n]))
    if [ $diff -ne 0 ] ; then
      [ $diff -le 0 ] && echo '-1' || echo '1'
      return
    fi
  done
  echo  '0'
} # ----------  end of function compareVersions  ----------
Fritz G. Mehner
  • 16,550
  • 2
  • 34
  • 41
  • fgm thanks for the script! Just because I'm a geek and have a need to make things exact "$diff -le 0" should be "$diff -lt 0". Since $diff would never be 0 at this point because of the previous conditional. – Steve Wall Sep 26 '12 at 14:21
7

Maybe you could use awk?

echo $VER_1 $VER2 | \
awk '{ split($1, a, ".");
       split($2, b, ".");
       for (i = 1; i <= 4; i++)
           if (a[i] < b[i]) {
               x =-1;
               break;
           } else if (a[i] > b[i]) {
               x = 1;
               break;
           }
       print x;
     }'

There isn't a perfect way to do this. As shown you could use array / loop for the numbers, also in bash.

schot
  • 10,958
  • 2
  • 46
  • 71
  • 1
    I think this is my preferred method for compatibility, as `sort -V` isn't widely supported enough, and using `awk` avoids the need for specific built-in features. – Haravikk Jul 29 '13 at 11:13
  • 1
    You could make this a little shorter by changing the `for` loop to `for (i = 1; !x && i <= 4; ++i) x = (a[i] < b[i]) ? -1 : ((a[i] > b[i]) ? 1 : 0);` – ldav1s Jan 17 '14 at 01:32
4

You can use sort -V to sort the lines with versions and match your version against the output:

% cat sorttest 
#!/bin/sh

version_lt() {
  echo "$1\n$2" | sort -V | head -n 1 | grep -q "$1"
}

display_versioncmp() {
  version_lt "$1" "$2" && echo "$1 < $2" || echo "$1 > $2"
}

X="1.2.3"
Y="11.2.3"
Z="1.22.3"

display_versioncmp "$X" "$Y"
display_versioncmp "$Y" "$X"
display_versioncmp "$X" "$Z"
display_versioncmp "$Z" "$X"
display_versioncmp "$Z" "$Y"
display_versioncmp "$Y" "$Z"

% ./sorttest 
1.2.3 < 11.2.3
11.2.3 > 1.2.3
1.2.3 < 1.22.3
1.22.3 > 1.2.3
1.22.3 < 11.2.3
11.2.3 > 1.22.3
timurb
  • 5,405
  • 2
  • 22
  • 17
  • 2
    Unfortunately, not every platform's bash has `sort -V`. (E.g., Mac may not.) – kcrisman Mar 30 '13 at 03:05
  • Script is not correct. It does not solve all the cases like 3.22.3 > 11.2.3 if input is 3 and 11 then definitely 11 is higher version but script shows 3 has higher version. – Andy Oct 06 '15 at 12:54
  • 1
    @AnandChoubey what version of sort do you have? With mine (that is Ubuntu 12.04) all works as expected: 3.22.3 > 1.2.3 3.22.3 < 11.2.3 # sort --version sort (GNU coreutils) 8.13 – timurb Oct 07 '15 at 14:55
2

If you can cheat by using Perl in your shell script, try it's built-in handling of version strings with string comparison operators:

V1=1.1.3; V2=1.1
echo $(perl -e '($x,$y)=@ARGV; print $x cmp $y' $V1 $V2)

You could also do away with the Perl variables and just use shift:

result=$(perl -e 'print shift cmp shift' $V1 $V2)

But that fails on versions > 10. So you could try this instead:

perl -e '($a,$b)=@ARGV; for ($a,$b) {s/(\d+)/sprintf "%5d", $1/ge}; print $a cmp $b;' 12.1.3 9.0.2

The sprintf of "%5d" is to make sure it will even work for Firefox, until version 99999... :-)

Obviously, you could also use the other Perl string operators like gt, lt, ge and le.

mivk
  • 13,452
  • 5
  • 76
  • 69
0

I had this problem, and after solving it looked to see if there was a better answer already available. My version allows for comparing different length version strings, and is the version_ge() function below, which should be used as a "greater or equal to" operator, as in

if version_ge "$version" "1.2.3.4"; then ...

#!/bin/sh

# Usage: split "<word list>" <variable1> <variable2>...
# Split a string of $IFS seperated words into individual words, and
# assign them to a list of variables. If there are more words than
# variables then all the remaining words are put in the last variable;
# use a dummy last variable to collect any unwanted words.
# Any variables for which there are no words are cleared.
# eg.   split 'hello Fred this is Bill' greeting who extra
# sets  greeting=hello  who=Fred  extra="this is Bill"
# and   split "$list" word list   # "pops" the first word from a list
split()
{
    # Prefix local names with the function name to try to avoid conflicts
    # local split_wordlist
    split_wordlist="$1"
    shift
    read "$@" <<EOF-split-end-of-arguments
${split_wordlist}
EOF-split-end-of-arguments
}


# Usage: version_ge v1 v2
# Where v1 and v2 are multi-part version numbers such as 12.5.67
# Missing .<number>s on the end of a version are treated as .0, & leading
# zeros are not significant, so 1.2 == 1.2.0 == 1.2.0.0 == 01.2 == 1.02
# Returns true if v1 >= v2, false if v1 < v2
version_ge()
{
    # Prefix local names with the function name to try to avoid conflicts
    # local version_ge_1 version_ge_2 version_ge_a version_ge_b
    # local version_ge_save_ifs
    version_ge_v1="$1"
    version_ge_v2="$2"

    version_ge_save_ifs="$IFS"
    while test -n "${version_ge_v1}${version_ge_v2}"; do
        IFS="."
        split "$version_ge_v1" version_ge_a version_ge_v1
        split "$version_ge_v2" version_ge_b version_ge_v2
        IFS="$version_ge_save_ifs"
        #echo " compare  $version_ge_a  $version_ge_b"
        test "0$version_ge_a" -gt "0$version_ge_b" && return 0 # v1>v2: true
        test "0$version_ge_a" -lt "0$version_ge_b" && return 1 # v1<v2:false
    done
    # version strings are both empty & no differences found - must be equal.
    return 0 # v1==v2: true
}
0

Here is a slightly improved method previously posted by schot. This method can save your life when there is no bash, sort -V commands etc. (for instance, in some docker images).

compareVersions() {
echo $1 $2 | \
awk '{ split($1, a, ".");
       split($2, b, ".");
       res = -1;
       for (i = 1; i <= 3; i++){
           if (a[i] < b[i]) {
               res =-1;
               break;
           } else if (a[i] > b[i]) {
               res = 1;
               break;
           } else if (a[i] == b[i]) {
               if (i == 3) {
               res = 0;
               break;
               } else {
               continue;
               }
           }
       }
       print res;
     }'
}