38

I have this script that should make sure that the users current PHP version is between a certain range, though it SHOULD work, there is a bug somewhere that makes it think that the version is out of range, could someone take a look and tell me what I can do to fix it?

function version { echo "$@" | gawk -F. '{ printf("%d.%d.%d\n", $1,$2,$3); }'; }

phpver=`php -v |grep -Eow '^PHP [^ ]+' |gawk '{ print $2 }'`

if [ $(version $phpver) > $(version 5.2.13) ] || [ $(version $phpver) < $(version 5.2.13) ]; then
  echo "PHP Version $phpver must be between 5.2.13 - 5.3.15"
  exit
fi
peterh
  • 11,875
  • 18
  • 85
  • 108
ehime
  • 8,025
  • 14
  • 51
  • 110
  • Can you provide an input which produces incorrect output? – chepner Jun 07 '13 at 17:18
  • 1
    Have you tried running this with the shell debugging feature `set -vx`? Seems like then it will be easy to see where the problem is. Also, I don't see that your `version` function is adding any value, it looks it will reformat 5.1.3 right back to 5.1.3. Oh.. is that it normalizes something like `5.01.03` to `5.1.3`? Good luck! – shellter Jun 07 '13 at 17:20
  • 1
    Even assuming your test with `>` worked, your test is checking whether the version is exactly 5.2.13 (because you're attempting to look for greater than 5.2.13 or less than 5.2.13). I think you run into I/O redirection issues because you're using `[`; the rules would be difference with `[[`. You will have a file 5.12.3 (empty) after the first test; it is fortunate (or do I mean unfortunate) that its there since it prevents the input redirection in the second test from failing. – Jonathan Leffler Jun 07 '13 at 17:30
  • @shellter great idea, I hadn't thought of that – ehime Jun 07 '13 at 17:34
  • 4
    possible duplicate of [Bash. How compare two strings in "version" format](http://stackoverflow.com/questions/4023830/bash-how-compare-two-strings-in-version-format) – Ilmari Karonen Apr 17 '15 at 14:24

10 Answers10

105

Here's how to compare versions.

using sort -V:

function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }

example usage:

first_version=5.100.2
second_version=5.1.2
if version_gt $first_version $second_version; then
     echo "$first_version is greater than $second_version !"
fi

pro:

  • solid way to compare fancy version strings:
    • support any length of sub-parts (ie: 1.3alpha.2.dev2 > 1.1 ?)
    • support alpha-betical sort (ie: 1.alpha < 1.beta2)
    • support big size version (ie: 1.10003939209329320932 > 1.2039209378273789273 ?)
  • can easily be modified to support n arguments. (leaved as an exercise ;) )
    • usually very usefull with 3 arguments: (ie: 1.2 < my_version < 2.7 )

cons:

  • uses a lot of various calls to different programs. So it's not that efficient.
  • uses a pretty recent version of sort and it might not be available on your system. (check with man sort)

without sort -V:

## each separate version number must be less than 3 digit wide !
function version { echo "$@" | gawk -F. '{ printf("%03d%03d%03d\n", $1,$2,$3); }'; }

example usage:

first_version=5.100.2
second_version=5.1.2
if [ "$(version "$first_version")" -gt "$(version "$second_version")" ]; then
     echo "$first_version is greater than $second_version !"
fi

pro:

  • quicker solution as it only calls 1 subprocess
  • much more compatible solution.

cons:

  • quite specific, version string must:
    • have version with 1, 2 or 3 parts only. (excludes '2.1.3.1')
    • each parts must be numerical only (excludes '3.1a')
    • each part can't be greater than 999 (excludes '1.20140417')

Comments about your script:

I can't see how it could work:

  • as stated in a comment > and < are very special shell character and you should replace them by -gt and -lt
  • even if you replaced the characters, you can't compare version numbers as if they where integers or float. For instance, on my system, php version is 5.5.9-1ubuntu4.

But your function version() is quite cleverly written already and may help you by circumventing the classical issue that sorting alphabetically numbers won't sort numbers numerically ( alphabetically 1 < 11 < 2, which is wrong numerically). But be carefull: arbitrarily large numbers aren't supported by bash (try to keep under 32bits if you aim at compatibility with 32bits systems, so that would be 9 digit long numbers). So I've modified your code (in the second method NOT using sort -V) to force only 3 digits for each part of the version string.

EDIT: applied @phk amelioration, as it is noticeably cleverer and remove a subprocess call in the first version using sort. Thanks.

vaab
  • 9,685
  • 7
  • 55
  • 60
  • 2
    You can replace `echo "$@" | tr " " "\n"` with `printf '%s\n' "$@"`, works better (e.g. if the version number string were to begin with `-`) and requires only one call. – phk Nov 05 '16 at 10:55
  • In addition, `printf` is a built-in, whereas `tr` is an external call, so phk's solution promises to be quite a bit faster. While we're reducing external calls, what about replacing your `gawk` pipe with `IFS=. read a b c <<<"$@"; printf "%03d" "$a" "$b" "$c"` ? – ghoti Nov 14 '16 at 08:28
  • @ghoti I had thought of using ``read`` to remove the ``tail`` from ``sort`` version as well with the same idea in mind, but it seem it is not faster on my computer (using ``IFS=`` might not be as fast as expected). TBH, this failure removed my good resolutions and it didn't push me to look at the second non-``sort`` version and the ``gawk``. However, if you have the time to do some nice real profiling test that show that you have a real edge over the current version, I'll add them gladly to my answer and would be glad to credit you. – vaab Nov 14 '16 at 21:48
  • idk why i never origionally plus one'd this. plus one to you sir. – ehime Aug 08 '17 at 21:38
  • 1
    yup, `sort -V` is not available under busybox – phil294 Dec 11 '17 at 15:19
  • For cases when sort -v can't be used printf("%03d%03d%03d\n", $1,$2,$3) generates version numbers such as 001001001. The problem is that bash considers numbers starting by 0 as being in octal and this prevents versions from including 8s or 9s. I prefer to use printf("%03s%03s%03s\n", $1,$2,$3) instead and use a lexicographical comparison and not an aritmetic one – Pedro Jun 15 '18 at 08:33
  • Note, `sort -V` thinks that `3.6.0 < 3.6.0-rc1` which may be problematic in some cases. – cladmi Aug 06 '18 at 12:54
19

There is possibility for deb-distributions:

dpkg --compare-versions <version> <relation> <version>

For example:

dpkg --compare-versions "0.0.4" "gt" "0.0.3"
if [ $? -eq "0" ]; then echo "YES"; else echo "NO"; fi
muru
  • 4,723
  • 1
  • 34
  • 78
Andrey
  • 1,495
  • 17
  • 14
4

It is doing a lexical comparison. Use one of these:

if [ $(version $phpver) -gt $(version 5.2.13) ] || [ $(version $phpver) -lt $(version 5.2.13) ]; then
if [[ $(version $phpver) > $(version 5.2.13) ]] || [[ $(version $phpver) < $(version 5.2.13) ]]; then
if (( $(version $phpver) > $(version 5.2.13) )) || (( $(version $phpver) < $(version 5.2.13) )); then

Or do it all in awk or some other tool. It is screaming for some optimisation. It also seems you're not producing numbers either, so you have a pretty odd design. Usually the version substrings are multiplied by 1000 and then all summed up to get a single comparable scalar.

lynxlynxlynx
  • 1,371
  • 17
  • 26
3

A much safer option for testing the PHP CLI version is to use PHP's own version_compare function.

#!/bin/bash

MIN_VERSION="7.0.0"
MAX_VERSION="7.1.4"
PHP_VERSION=`php -r 'echo PHP_VERSION;'`

function version_compare() {
    COMPARE_OP=$1;
    TEST_VERSION=$2;
    RESULT=$(php -r 'echo version_compare(PHP_VERSION, "'${TEST_VERSION}'", "'${COMPARE_OP}'") ? "TRUE" : "";')

    test -n "${RESULT}";
}

if ( version_compare "<" "${MIN_VERSION}" || version_compare ">" "${MAX_VERSION}" ); then
    echo "PHP Version ${PHP_VERSION} must be between ${MIN_VERSION} - ${MAX_VERSION}";
    exit 1;
fi

echo "PHP Version ${PHP_VERSION} is good!";
ianmjones
  • 3,395
  • 1
  • 25
  • 26
2

Here's another solution that:

  • does not run any external command apart from tr
  • has no restriction on number of parts in version string
  • can compare version strings with different number of parts

Note that it's Bash code using array variables.

compare_versions()
{
    local v1=( $(echo "$1" | tr '.' ' ') )
    local v2=( $(echo "$2" | tr '.' ' ') )
    local len="$(max "${#v1[*]}" "${#v2[*]}")"
    for ((i=0; i<len; i++))
    do
        [ "${v1[i]:-0}" -gt "${v2[i]:-0}" ] && return 1
        [ "${v1[i]:-0}" -lt "${v2[i]:-0}" ] && return 2
    done
    return 0
}

The function returns:

  • 0 if versions are equal (btw: 1.2 == 1.2.0)
  • 1 if the 1st version is bigger / newer
  • 2 if the 2nd version is bigger / newer

However #1 -- it requires one additional function (but function min is quite usable to have anyway):

min()
{
    local m="$1"
    for n in "$@"
    do
        [ "$n" -lt "$m" ] && m="$n"
    done
    echo "$m"
}

However #2 -- it cannot compare version strings with alpha-numeric parts (though that would not be difficult to add, actually).

mato
  • 593
  • 4
  • 11
1

The following solution should more accurately addresses your exact need. It can be used to test whether the CURRENT version string falls between MIN and MAX. I am assuming that MIN and MAX are acceptable version numbers (i.e. MIN <= CURRENT <= MAX rather than MIN < CURRENT < MAX).

# Usage: version MIN CURRENT MAX
version(){
    local h t v

    [[ $2 = "$1" || $2 = "$3" ]] && return 0

    v=$(printf '%s\n' "$@" | sort -V)
    h=$(head -n1 <<<"$v")
    t=$(tail -n1 <<<"$v")

    [[ $2 != "$h" && $2 != "$t" ]]
}

For example...

if ! version 5.2.13 "$phpver" 5.3.15; then
    echo "PHP Version $phpver must be between 5.2.13 - 5.3.15"
    exit
fi
Six
  • 5,122
  • 3
  • 29
  • 38
1

If you're on Bash 3 with an older version of sort (lookin at you macOS...), then I created the following helper script you can source in (can also be ran as a command):

https://github.com/unicorn-fail/version_compare

Mark Carver
  • 136
  • 3
  • 11
0

I wrote this inelegant function a while back for a similar problem. vers_limit will return 0 if arg1 is less than or equal to arg2, non-0 otherwise:

vers_limit()
{
VERNEW=$1
VERLMT=$2

CHKNEW=$VERNEW
CHKLMT=$VERLMT

PASSED=

while :
do
        PARTNEW=${CHKNEW%%.*}
        PARTLMT=${CHKLMT%%.*}
        if [[ $PARTNEW -lt $PARTLMT ]]
        then
                PASSED=GOOD
                break
        elif [[ $PARTNEW -gt $PARTLMT ]]
        then
                PASSED=
                break
        else
                NXTNEW=${CHKNEW#*.}
                if [[ $NXTNEW == $CHKNEW ]]
                then
                        if [[ $NXTNEW == $CHKLMT ]]
                        then
                                PASSED=GOOD
                                break
                        else
                                NXTNEW=0
                        fi
                fi
                NXTLMT=${CHKLMT#*.}
                if [[ $NXTLMT == $CHKLMT ]]
                then
                        NXTLMT=0
                fi
        fi
        CHKNEW=$NXTNEW
        CHKLMT=$NXTLMT
done
test "$PASSED"
}

This is not as compact as some of the other solutions here, nor does it provide 3-way status (i.e., less, equal, greater), though I believe one can order the arguments, interpret the pass/fail status, and/or call twice to accomplish any desired result. That said, vers_limit does have certain virtues:

  1. No calls to external utilities such as sort, awk, gawk, tr, etc.

  2. Handles numeric versions of arbitrary size (up to the shell's limit for integer calculations)

  3. Handles an arbitrary number of "dotted" parts.

oracleif
  • 21
  • 5
0
v_min="5.2.15"
v_max="5.3.15"
v_php="$(php -v | head -1 | awk '{print $2}')"

[ ! "$v_php" = "$(echo -e "$v_php\n$v_min\n$v_max" | sort -V | head -2 | tail -1)" ] && {
  echo "PHP Version $v_php must be between $v_min - $v_max"
  exit
}

This puts v_min, v_max and v_php in version order and tests if v_php is not in the middle.

ehime
  • 8,025
  • 14
  • 51
  • 110
5p0ng3b0b
  • 511
  • 4
  • 10
0

PURE BASH

I found my way to this page because I had the same problem. The other answers did not satisfy me, so I wrote this function.
So long as the 2 versions have the same number of periods in them this will compare the versions correctly.
It does a c style for loop, setting $i incrementally from 0 to # of numbers in the version string.
for each #:
if new - old is neg we know the first version is newer.
If new - old is pos we know the first version is older.
If new - old is 0 then it is the same and we need to continue checking.
We run false after to set exit status of the function for the case where $1 == $2 the versions are totally identical.

newver=1.10.1
installedver=1.9.25
#installedver=1.11.25
#installedver=1.10.1

checkupdate(){
# $1 = new version
# $2 = installed version
IFS='.' read -r -a nver <<< "$1"
IFS='.' read -r -a iver <<< "$2"
for ((i = 0 ; i < "${#nver[@]}" ; i++)) ;do
 case "$((${nver[i]}-${iver[i]}))" in
  -*) return 1 ;;
   0) ;;
   *) return 0 ;;
 esac
 false
done
}
checkupdate "$newver" "$installedver" && echo yes || echo no

Another method for SH

After I tried to implement my above function on Android I realized that I would not always have bash, so the above function did not work for me. Here is the version I wrote using awk to get around needing bash:

checkupdate(){
# $1 = new version
# $2 = installed version

i=1
#we start at 1 and go until number of . so we can use our counter as awk position
places=$(awk -F. '{print NF+1}' <<< "$1")
while (( "$i" < "$places" )) ;do
 npos=$(awk -v pos=$i -F. '{print $pos}' <<< "$1")
 ipos=$(awk -v pos=$i -F. '{print $pos}' <<< "$2")
 case "$(( $npos - $ipos ))" in
  -*) return 1 ;;
   0) ;;
   *) return 0 ;;
 esac
 i=$((i+1))
 false
done
}