273

Is there any way to compare such strings on bash, e.g.: 2.4.5 and 2.8 and 2.4.5.1?

starball
  • 20,030
  • 7
  • 43
  • 238
exabiche
  • 2,895
  • 2
  • 17
  • 10

38 Answers38

272

Here is a pure Bash version that doesn't require any external utilities:

#!/bin/bash
vercomp () {
    if [[ $1 == $2 ]]
    then
        return 0
    fi
    local IFS=.
    local i ver1=($1) ver2=($2)
    # fill empty fields in ver1 with zeros
    for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
    do
        ver1[i]=0
    done
    for ((i=0; i<${#ver1[@]}; i++))
    do
        if [[ -z ${ver2[i]} ]]
        then
            # fill empty fields in ver2 with zeros
            ver2[i]=0
        fi
        if ((10#${ver1[i]} > 10#${ver2[i]}))
        then
            return 1
        fi
        if ((10#${ver1[i]} < 10#${ver2[i]}))
        then
            return 2
        fi
    done
    return 0
}

testvercomp () {
    vercomp $1 $2
    case $? in
        0) op='=';;
        1) op='>';;
        2) op='<';;
    esac
    if [[ $op != $3 ]]
    then
        echo "FAIL: Expected '$3', Actual '$op', Arg1 '$1', Arg2 '$2'"
    else
        echo "Pass: '$1 $op $2'"
    fi
}

# Run tests
# argument table format:
# testarg1   testarg2     expected_relationship
echo "The following tests should pass"
while read -r test
do
    testvercomp $test
done << EOF
1            1            =
2.1          2.2          <
3.0.4.10     3.0.4.2      >
4.08         4.08.01      <
3.2.1.9.8144 3.2          >
3.2          3.2.1.9.8144 <
1.2          2.1          <
2.1          1.2          >
5.6.7        5.6.7        =
1.01.1       1.1.1        =
1.1.1        1.01.1       =
1            1.0          =
1.0          1            =
1.0.2.0      1.0.2        =
1..0         1.0          =
1.0          1..0         =
EOF

echo "The following test should fail (test the tester)"
testvercomp 1 1 '>'

Run the tests:

$ . ./vercomp
The following tests should pass
Pass: '1 = 1'
Pass: '2.1 < 2.2'
Pass: '3.0.4.10 > 3.0.4.2'
Pass: '4.08 < 4.08.01'
Pass: '3.2.1.9.8144 > 3.2'
Pass: '3.2 < 3.2.1.9.8144'
Pass: '1.2 < 2.1'
Pass: '2.1 > 1.2'
Pass: '5.6.7 = 5.6.7'
Pass: '1.01.1 = 1.1.1'
Pass: '1.1.1 = 1.01.1'
Pass: '1 = 1.0'
Pass: '1.0 = 1'
Pass: '1.0.2.0 = 1.0.2'
Pass: '1..0 = 1.0'
Pass: '1.0 = 1..0'
The following test should fail (test the tester)
FAIL: Expected '>', Actual '=', Arg1 '1', Arg2 '1'
Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • 3
    Could you state explicitly license of this code snippet? Code looks perfect but I'm not sure if I can use it in AGPLv3 licensed project. – Kamil Dziedzic Jul 31 '14 at 23:31
  • 4
    @KamilDziedzic: The license terms are stated at the bottom of this page (and most others). – Dennis Williamson Aug 01 '14 at 01:08
  • 4
    https://www.gnu.org/licenses/license-list.html#ccbysa `Please don't use it for software or documentation, since it is incompatible with the GNU GPL` :/ but +1 for great code – Kamil Dziedzic Aug 01 '14 at 09:44
  • I get these errors on Mac, makes any sense? `10#0 1 > 10#0: syntax error in expression (error token is "1 > 10#0") 10#0 1 < 10#0: syntax error in expression (error token is "1 < 10#0")` version `GNU bash, version 3.2.51(1)-release (x86_64-apple-darwin13)` – Inserve Oct 06 '14 at 12:04
  • @MarnickMenting: Is that while running the test function over the provided test data? It looks like somehow a space is getting introduced into the data. I just copied and pasted the functions and test loop from my answer above into Bash 3.2 on Linux and it worked fine. Check yours to make sure some stray characters didn't get included. Also, try to narrow it down to see which particular input is causing the error. – Dennis Williamson Oct 06 '14 at 16:05
  • @DennisWilliamson I tested some values and found the problem. It was a combination of using a version number that started with zero (e.g. `0.2`), and quoting the input variables wrong (I did `vercomp "$1 $2"` instead of `vercomp "$1" "$2"` or just `vercomp $1 $2`) – Inserve Oct 07 '14 at 11:53
  • 3
    this fails '1.4rc2 > 1.3.3'. notice the alphanumeric version – Salimane Adjao Moustapha Dec 09 '14 at 11:52
  • 1
    @SalimaneAdjaoMoustapha: It's not designed to handle that type of version string. I don't see any other answers here that can handle that comparison. – Dennis Williamson Dec 09 '14 at 12:45
  • Please consider Joynes' solution further down. I believe it handles most things you throw at it, works just about anywhere and is basically a one liner. – LOAS Mar 11 '15 at 08:19
  • I'd also like to add in testvercomp, below the case: `if [[ $2 == '!=' ]] ; then if [[ $op != '=' ]] ; then # True return 0 fi # False return 1 fi` – Tanax May 19 '16 at 12:32
  • @Tanax: I'm afraid I don't understand what you're trying to do. `$2` will never equal `!=`. – Dennis Williamson May 19 '16 at 16:03
  • @DennisWilliamson: Right, sorry! In my implementation of this, I switched the arguments so that you call the testvercomp with 1 '<' 2, makes more sense to me. In your implementation, my addition would be if $3 == '!=' to allow for checking if version is "not" a specific version, regardless if it's less than or higher than. – Tanax May 22 '16 at 09:28
  • For an extended version of this with support for revisions (e.g. '1.0-r1'), see https://stackoverflow.com/a/49351294/1976617 – Fonic Mar 18 '18 at 18:12
  • Process completed with exit code 1 when `set -e` – jk2K Jan 21 '22 at 02:38
  • @jk2K: I recommend never using `set -e`. – Dennis Williamson Jun 24 '22 at 00:15
223

If you have coreutils-7 (in Ubuntu Karmic but not Jaunty) then your sort command should have a -V option (version sort) which you could use to do the comparison:

verlte() {
    [  "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
}

verlt() {
    [ "$1" = "$2" ] && return 1 || verlte $1 $2
}

verlte 2.5.7 2.5.6 && echo "yes" || echo "no" # no
verlt 2.4.10 2.4.9 && echo "yes" || echo "no" # no
verlt 2.4.8 2.4.10 && echo "yes" || echo "no" # yes
verlte 2.5.6 2.5.6 && echo "yes" || echo "no" # yes
verlt 2.5.6 2.5.6 && echo "yes" || echo "no" # no

Since GNU sort additionally has -C (--check=silent) which just reports (via exit status) whether the input is in sorted order, we don't actually need to capture output and test that - we could write

verlte() {
    printf '%s\n' "$1" "$2" | sort -C -V
}

verlt() {
    ! verlte "$2" "$1"
}

We can extend this technique to determine whether a version lies in the range specified by two (inclusive) limits:

ver_between() {
    # args: min, actual, max
    printf '%s\n' "$@" | sort -C -V
}
Toby Speight
  • 27,591
  • 48
  • 66
  • 103
kanaka
  • 70,845
  • 23
  • 144
  • 140
  • 7
    Nice solution. For Mac OSX users, you can use GNU Coreutils gsort. That's available through homebrew: `brew install coreutils`. Then the above should just be modified to use gsort. – justsee Apr 01 '13 at 04:33
  • I got it working in a script in Ubuntu precise by removing -e from echo. – Hannes R. Mar 31 '14 at 12:38
  • 4
    Doesn't work with e.g. Busybox on an embedded Linux system, because [Busybox `sort`](http://www.busybox.net/downloads/BusyBox.html#sort) doesn't have `-V` option. – Craig McQueen Jul 01 '15 at 04:47
  • @CraigMcQueen `-V` is using [`filevercmp`](http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/sort.c?id=v8.23#n2586) from [`gnulib`](http://git.savannah.gnu.org/cgit/gnulib.git/tree/lib/filevercmp.c?id=71be4c87c8267369f40fbfab7523ab9847154c02#n125). Interestingly, not the `strverscmp` from glibc: http://stackoverflow.com/a/37015135/895245 – Ciro Santilli OurBigBook.com May 04 '16 at 13:10
  • 8
    It's better to use `printf` instead of `echo -e`. – phk Nov 05 '16 at 10:53
  • 1
    Doesn't work with different representations of the same version (e.g., `2` and `2.0`). – Will Vousden Jun 11 '17 at 19:22
  • @WillVousden seems to work for me with coreutils-8.25: 2 is consistently sorted before 2.0 which seems reasonable. Since 2 and 2.0 are the "same" there are three options: stable sort (incoming order), pick an arbitrary one to always be first (the behavior I see), or random every time. The first two seem reasonable and predictable choices, the last one would be unexpected and probably not a good choice for a tool that might be used in scripting. What behavior are you expecting? – kanaka Jun 11 '17 at 21:45
  • I think the sort has to be stable for this to work sensibly. For example: `verlte 2 2.0` succeeds, while `verlte 2.0 2` fails, for the reasons you outline (`2` always comes before `2.0`). I would expect these to produce the same result. – Will Vousden Jun 13 '17 at 08:31
  • A quick note regarding ``~`` support. In RPM and deb packaging, ``~`` sorts before any character, including empty. I.e., ``2.0~beta1 < 2.0``. ``sort -V`` supports this convention – Chris Cogdon Jul 19 '18 at 17:23
  • Heh, that's the first solution what came in mind before I started googling. I thought there is something better within unix utils I don't know yet. – Sergey Oct 03 '18 at 15:24
  • 16
    GNU `sort` also has `-C` or `--check=silent`, so you can write `verlte() { printf '%s\n%s' "$1" "$2" | sort -C -V }`; and checking strict less than is more simply done as `verlt() { ! verlte "$2" "$1" }`. – Toby Speight Oct 19 '18 at 10:27
  • @justsee, `sort` in the newest macOS has supported option `-V`, no need to install `gsort` (the GNU coreutils). @CraigMcQueen, the newest `busybox sort` has supported option `-V`. – Bruce Oct 11 '21 at 21:43
  • Hi, script newbie here. How should I use this on a "if - else" situation? I have tried this: if [ verlte $VERSION $version_check ] then echo "yes" else echo "no" fi And it says: [: 0.1: binary operator expected Thanks! – Iñigo Gorosabel Dec 28 '21 at 21:41
  • 3
    Aren't backticks an outdated syntax? – Martynas Jusevičius Mar 06 '22 at 17:47
87

There probably is no universally correct way to achieve this. If you are trying to compare versions in the Debian package system try dpkg --compare-versions <first> <relation> <second>.

Helmut Grohne
  • 6,578
  • 2
  • 31
  • 67
  • 17
    Usage: `dpkg --compare-versions "1.0" "lt" "1.2"` means 1.0 less than 1.2. The comparison result `$?` is `0` if true so you can use it directly after `if` statement. – KrisWebDev Mar 27 '16 at 15:32
77

GNU sort has an option to sort version numbers:

printf '2.4.5\n2.8\n2.4.5.1\n' | sort -V

gives:

2.4.5
2.4.5.1
2.8

And in combination with -C we are able to compare versions (thanks to the comment of @WolframRösler):

if ! printf '7.18\n%s\n' "$(curl -V | grep -io "[0-9][0-9a-z.-]*" | head -n1)" | sort -V -C; then
  echo "Error: curl version is older than 7.18!"
else
  echo "curl version is at least 7.18."
fi

Explanation:

  • -C checks if the obtained lines are already sorted and if not it returns the exit status 1 (= version is not equal or older)
  • grep -io "[0-9][0-9a-z.-]*" | head -n1 catches the first version string returned by command -V which must start with a number and can be followed by alphanumeric characters, dot and hyphen which covers even versions like 6.2-RC3
mgutt
  • 5,867
  • 2
  • 50
  • 77
mouviciel
  • 66,855
  • 13
  • 106
  • 140
  • 2
    The question seems to be about version sort. Consider: `echo -e "2.4.10\n2.4.9" | sort -n -t.` – kanaka Oct 26 '10 at 13:46
  • 2
    sorting this numerically is not right. You would need to at least normalize the strings first. – frankc Oct 26 '10 at 19:44
  • 6
    Doesn't work with e.g. Busybox on an embedded Linux system, because [Busybox `sort`](http://www.busybox.net/downloads/BusyBox.html#sort) doesn't have `-V` option. – Craig McQueen Jul 01 '15 at 04:59
  • 2
    It's worth noting that if the version number can be anything then would be better to use it in the form `printf '%s\n' "2.4.5" "2.8" "2.4.5.1" | sort -V`. – phk Nov 05 '16 at 10:58
  • 1
    As noted in [another answer](https://stackoverflow.com/a/4024263/648265), this only works with `coreutils 7+`. – ivan_pozdeev Dec 24 '17 at 08:30
  • 2
    @CraigMcQueen, the newest `busybox sort` has supported option `-V`. – Bruce Oct 11 '21 at 21:39
  • 2
    Good answer and you can add `-C` for comparision instead of sorting, e. g. `printf '2.4.5\n2.4.10\n' | sort -V -C`. Outputs nothing, exit status is 0 if the first version number is older than the second, 1 if it is newer. – Wolfram Rösler Jun 27 '23 at 15:48
73
function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }

Used as such:

if [ $(version $VAR) -ge $(version "6.2.0") ]; then
    echo "Version is up to date"
fi

(from https://apple.stackexchange.com/a/123408/11374)

Note: this function currently supports versions with up to four components. Can be extended trivially if necessary.

yairchu
  • 23,680
  • 7
  • 69
  • 109
49

Well if you know the number of fields you can use -k n,n and get a super-simple solution

echo '2.4.5
2.8
2.4.5.1
2.10.2' | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -k 4,4 -g

2.4.5
2.4.5.1
2.8
2.10.2
joynes
  • 1,627
  • 4
  • 16
  • 19
  • 8
    four years late to the party, but my favorite solution by far :) – LOAS Mar 11 '15 at 08:15
  • yeah, the `-t` option only accepts single character tabs...otherwise, `2.4-r9` would work as well. What a shame :/ – scottysseus Jul 07 '15 at 14:53
  • 3
    For Solaris compat, I had to change `-g` to `-n`. Any reason why not to for this example? On a side-note... to perform a "greater than" type comparison, you can check if the desired sort is the same as the actual sort... e.g. `desired="1.9\n1.11"; actual="$(echo -e $desired |sort -t '.' -k 1,1 -k 2,2 -g)";` and then verify `if [ "$desired" = "$actual" ]`. – tresf Jan 16 '16 at 06:16
24

This is for at most 4 fields in the version.

$ function ver { printf "%03d%03d%03d%03d" $(echo "$1" | tr '.' ' '); }
$ [ $(ver 10.9) -lt $(ver 10.10) ] && echo hello  
hello
codeforester
  • 39,467
  • 16
  • 112
  • 140
fakedrake
  • 6,528
  • 8
  • 41
  • 64
  • 3
    In case the version could also have 5 fields, the above could be made safe like this: `printf "%03d%03d%03d%03d" $(echo "$1" | tr '.' '\n' | head -n 4)` – robinst Apr 20 '15 at 04:57
  • 2
    Not sure if it all applies to all versions of bash, but in my case a semicolon is missing after the last round bracket. – Holger Brandl Aug 11 '17 at 17:26
  • 1
    @robinst For `head -n` to work, I had to change to `tr '.' '\n'` – Victor Sergienko Oct 13 '17 at 21:04
  • Added the semicolon. – codeforester Feb 26 '18 at 23:22
  • 1
    Slightly better: `tr -cs '0-9' ' '` to scrub out and separate hyphens, commas, and other delimiters. – Otheus Jun 13 '18 at 17:24
  • 1
    @OleksiiChekulaiev Pipe `tr` output through `sed 's/\(^\| \)0\([0-9][0-9]*\)/\1\2/g'` which will take care of that (Rather clumsily) – Otheus Jun 13 '18 at 17:32
  • 1
    @Otheus I used `awk` instead. `ver () { echo "$@" | awk -F. '{ printf("%d%03d%03d", $1,$2,$3); }'; }` It worked perfectly for my needs – Oleksii Chekulaiev Jun 13 '18 at 19:32
  • @robinst increasing to 5 fields give the following error: `value too great for base` – Tom Kelly Apr 15 '20 at 03:27
  • The most straightforward solution to compare SemVer version core, i.e. without pre-release or build suffixes like "rc.1" or "beta". Interesting that "echo | tr" makes it POSIX (if you leave out the Bash-only keyword "function"), for Bash-IX you can use `${1//./ }`(no subshell, no externals). – hh skladby Dec 14 '21 at 21:44
  • @codeforester Worth to mention that the semicolon is needed if there is no line break before the closing "}", not for a certain functionality here – hh skladby Dec 14 '21 at 21:46
  • @TomKelly Which program / shell does report this? If your program / shell can only handle 32-bit integers, you are bound to max "2.147.483.647" for three digit fields, ie. even 4 fields will not fully work, with 64-bit (eg. since Bash 2) you have max "9.223.372.036.854.775.807" or up to 6 full fields – hh skladby Dec 15 '21 at 11:51
  • 1
    Note that you can (or must) *adapt the "positional fill"* `%03d` to `%04d`or `%02d`. So you could handle Googe Chrome versions like "96.0.4664.45" by `%03d%02d%03d – hh skladby Dec 15 '21 at 11:59
  • `printf` applies for each parameter. don't need to format each one indlvidually. Also, use bash parameter expansion to replace `.` for spaces: `function ver { printf "%03d" ${1//./ }; }` – caruccio Jun 18 '22 at 15:52
13
  • Function V - pure bash solution, no external utilities required.
  • Supports = == != < <= > and >= (lexicographic).
  • Optional tail letter comparison: 1.5a < 1.5b
  • Unequal length comparison: 1.6 > 1.5b
  • Reads left-to-right: if V 1.5 '<' 1.6; then ....

<>

# Sample output
# Note: ++ (true) and __ (false) mean that V works correctly.

++ 3.6 '>' 3.5b
__ 2.5.7 '<=' 2.5.6
++ 2.4.10 '<' 2.5.9
__ 3.0002 '>' 3.0003.3
++ 4.0-RC2 '>' 4.0-RC1

<>

function V() # $1-a $2-op $3-$b
# Compare a and b as version strings. Rules:
# R1: a and b : dot-separated sequence of items. Items are numeric. The last item can optionally end with letters, i.e., 2.5 or 2.5a.
# R2: Zeros are automatically inserted to compare the same number of items, i.e., 1.0 < 1.0.1 means 1.0.0 < 1.0.1 => yes.
# R3: op can be '=' '==' '!=' '<' '<=' '>' '>=' (lexicographic).
# R4: Unrestricted number of digits of any item, i.e., 3.0003 > 3.0000004.
# R5: Unrestricted number of items.
{
  local a=$1 op=$2 b=$3 al=${1##*.} bl=${3##*.}
  while [[ $al =~ ^[[:digit:]] ]]; do al=${al:1}; done
  while [[ $bl =~ ^[[:digit:]] ]]; do bl=${bl:1}; done
  local ai=${a%$al} bi=${b%$bl}

  local ap=${ai//[[:digit:]]} bp=${bi//[[:digit:]]}
  ap=${ap//./.0} bp=${bp//./.0}

  local w=1 fmt=$a.$b x IFS=.
  for x in $fmt; do [ ${#x} -gt $w ] && w=${#x}; done
  fmt=${*//[^.]}; fmt=${fmt//./%${w}s}
  printf -v a $fmt $ai$bp; printf -v a "%s-%${w}s" $a $al
  printf -v b $fmt $bi$ap; printf -v b "%s-%${w}s" $b $bl

  case $op in
    '<='|'>=' ) [ "$a" ${op:0:1} "$b" ] || [ "$a" = "$b" ] ;;
    * )         [ "$a" $op "$b" ] ;;
  esac
}

Code Explained

Line 1: Define local variables:

  • a, op, b - comparison operands and operator, i.e., "3.6" > "3.5a".
  • al, bl - letter tails of a and b, initialized to the tail item, i.e., "6" and "5a".

Lines 2, 3: Left-trim digits from the tail items so only letters are left, if any, i.e., "" and "a".

Line 4: Right trim letters from a and b to leave just the sequence of numeric items as local variables ai and bi, i.e., "3.6" and "3.5". Notable example: "4.01-RC2" > "4.01-RC1" yields ai="4.01" al="-RC2" and bi="4.01" bl="-RC1".

Line 6: Define local variables:

  • ap, bp - zero right-paddings for ai and bi. Start by keeping the inter-item dots only, of which number equals the number of elements of a and b respectively.

Line 7: Then append "0" after each dot to make padding masks.

Line 9: Local variables:

  • w - item width
  • fmt - printf format string, to be calculated
  • x - temporary
  • With IFS=. bash splits variable values at '.'.

Line 10: Calculate w, the maximum item width, which will be used to align items for lexicographic comparison. In our example w=2.

Line 11: Create the printf alignment format by replacing each character of $a.$b with %${w}s, i.e., "3.6" > "3.5a" yields "%2s%2s%2s%2s".

Line 12: "printf -v a" sets the value of variable a. This is equivalent to a=sprintf(...) in many programming languages. Note that here, by effect of IFS=. the arguments to printf split into individual items.

With the first printf items of a are left-padded with spaces while enough "0" items are appended from bp to ensure that the resulting string a can be meaningfully compared to a similarly formatted b.

Note that we append bp - not ap to ai because ap and bp may have different lenghts, so this results in a and b having equal lengths.

With the second printf we append the letter part al to a with enough padding to enable meaningful comparison. Now a is ready for comparison with b.

Line 13: Same as line 12 but for b.

Line 15: Split comparison cases between non-built-in (<= and >=) and built-in operators.

Line 16: If the comparison operator is <= then test for a<b or a=b - respectively >= a<b or a=b

Line 17: Test for built-in comparison operators.

<>

# All tests

function P { printf "$@"; }
function EXPECT { printf "$@"; }
function CODE { awk $BASH_LINENO'==NR{print " "$2,$3,$4}' "$0"; }
P 'Note: ++ (true) and __ (false) mean that V works correctly.\n'

V 2.5    '!='  2.5      && P + || P _; EXPECT _; CODE
V 2.5    '='   2.5      && P + || P _; EXPECT +; CODE
V 2.5    '=='  2.5      && P + || P _; EXPECT +; CODE

V 2.5a   '=='  2.5b     && P + || P _; EXPECT _; CODE
V 2.5a   '<'   2.5b     && P + || P _; EXPECT +; CODE
V 2.5a   '>'   2.5b     && P + || P _; EXPECT _; CODE
V 2.5b   '>'   2.5a     && P + || P _; EXPECT +; CODE
V 2.5b   '<'   2.5a     && P + || P _; EXPECT _; CODE
V 3.5    '<'   3.5b     && P + || P _; EXPECT +; CODE
V 3.5    '>'   3.5b     && P + || P _; EXPECT _; CODE
V 3.5b   '>'   3.5      && P + || P _; EXPECT +; CODE
V 3.5b   '<'   3.5      && P + || P _; EXPECT _; CODE
V 3.6    '<'   3.5b     && P + || P _; EXPECT _; CODE
V 3.6    '>'   3.5b     && P + || P _; EXPECT +; CODE
V 3.5b   '<'   3.6      && P + || P _; EXPECT +; CODE
V 3.5b   '>'   3.6      && P + || P _; EXPECT _; CODE

V 2.5.7  '<='  2.5.6    && P + || P _; EXPECT _; CODE
V 2.4.10 '<'   2.4.9    && P + || P _; EXPECT _; CODE
V 2.4.10 '<'   2.5.9    && P + || P _; EXPECT +; CODE
V 3.4.10 '<'   2.5.9    && P + || P _; EXPECT _; CODE
V 2.4.8  '>'   2.4.10   && P + || P _; EXPECT _; CODE
V 2.5.6  '<='  2.5.6    && P + || P _; EXPECT +; CODE
V 2.5.6  '>='  2.5.6    && P + || P _; EXPECT +; CODE
V 3.0    '<'   3.0.3    && P + || P _; EXPECT +; CODE
V 3.0002 '<'   3.0003.3 && P + || P _; EXPECT +; CODE
V 3.0002 '>'   3.0003.3 && P + || P _; EXPECT _; CODE
V 3.0003.3 '<' 3.0002   && P + || P _; EXPECT _; CODE
V 3.0003.3 '>' 3.0002   && P + || P _; EXPECT +; CODE

V 4.0-RC2 '>' 4.0-RC1   && P + || P _; EXPECT +; CODE
V 4.0-RC2 '<' 4.0-RC1   && P + || P _; EXPECT _; CODE
stepse
  • 341
  • 3
  • 6
12

You can recursively split on . and compare as shown in the following algorithm, taken from here. It returns 10 if the versions are the same, 11 if version 1 is greater than version 2 and 9 otherwise.

#!/bin/bash
do_version_check() {

   [ "$1" == "$2" ] && return 10

   ver1front=`echo $1 | cut -d "." -f -1`
   ver1back=`echo $1 | cut -d "." -f 2-`

   ver2front=`echo $2 | cut -d "." -f -1`
   ver2back=`echo $2 | cut -d "." -f 2-`

   if [ "$ver1front" != "$1" ] || [ "$ver2front" != "$2" ]; then
       [ "$ver1front" -gt "$ver2front" ] && return 11
       [ "$ver1front" -lt "$ver2front" ] && return 9

       [ "$ver1front" == "$1" ] || [ -z "$ver1back" ] && ver1back=0
       [ "$ver2front" == "$2" ] || [ -z "$ver2back" ] && ver2back=0
       do_version_check "$ver1back" "$ver2back"
       return $?
   else
           [ "$1" -gt "$2" ] && return 11 || return 9
   fi
}    

do_version_check "$1" "$2"

Source

dogbane
  • 266,786
  • 75
  • 396
  • 414
8

if it's just about to know whether one version is lower than another I came up checking whether sort --version-sort changes the order of my version strings:

    string="$1
$2"
    [ "$string" == "$(sort --version-sort <<< "$string")" ]
Hachi
  • 3,237
  • 1
  • 21
  • 29
6
$ for OVFTOOL_VERSION in "4.2.0" "4.2.1" "5.2.0" "3.2.0" "4.1.9" "4.0.1" "4.3.0" "4.5.0" "4.2.1" "30.1.0" "4" "5" "4.1" "4.3"
> do
>   if [ $(echo "$OVFTOOL_VERSION 4.2.0" | tr " " "\n" | sort --version-sort | head -n 1) = 4.2.0 ]; then 
>     echo "$OVFTOOL_VERSION is >= 4.2.0"; 
>   else 
>     echo "$OVFTOOL_VERSION is < 4.2.0"; 
>   fi
> done
4.2.0 is >= 4.2.0
4.2.1 is >= 4.2.0
5.2.0 is >= 4.2.0
3.2.0 is < 4.2.0
4.1.9 is < 4.2.0
4.0.1 is < 4.2.0
4.3.0 is >= 4.2.0
4.5.0 is >= 4.2.0
4.2.1 is >= 4.2.0
30.1.0 is >= 4.2.0
4 is < 4.2.0
5 is >= 4.2.0
4.1 is < 4.2.0
4.3 is >= 4.2.0
Dan Dye
  • 767
  • 7
  • 15
5

I implemented a function that returns the same results as Dennis Williamson's but uses fewer lines. It does perform a sanity check initially which causes 1..0 to fail from his tests (which I would argue should be the case) but all of his other tests pass with this code:

#!/bin/bash
version_compare() {
    if [[ $1 =~ ^([0-9]+\.?)+$ && $2 =~ ^([0-9]+\.?)+$ ]]; then
        local l=(${1//./ }) r=(${2//./ }) s=${#l[@]}; [[ ${#r[@]} -gt ${#l[@]} ]] && s=${#r[@]}

        for i in $(seq 0 $((s - 1))); do
            [[ ${l[$i]} -gt ${r[$i]} ]] && return 1
            [[ ${l[$i]} -lt ${r[$i]} ]] && return 2
        done

        return 0
    else
        echo "Invalid version number given"
        exit 1
    fi
}
v0rtex
  • 381
  • 3
  • 6
5

Here is a simple Bash function that uses no external commands. It works for version strings that have up to three numeric parts in them - less than 3 is fine as well. It can easily be extended for more. It implements =, <, <=, >, >=, and != conditions.

#!/bin/bash
vercmp() {
    version1=$1 version2=$2 condition=$3

    IFS=. v1_array=($version1) v2_array=($version2)
    v1=$((v1_array[0] * 100 + v1_array[1] * 10 + v1_array[2]))
    v2=$((v2_array[0] * 100 + v2_array[1] * 10 + v2_array[2]))
    diff=$((v2 - v1))
    [[ $condition = '='  ]] && ((diff == 0)) && return 0
    [[ $condition = '!=' ]] && ((diff != 0)) && return 0
    [[ $condition = '<'  ]] && ((diff >  0)) && return 0
    [[ $condition = '<=' ]] && ((diff >= 0)) && return 0
    [[ $condition = '>'  ]] && ((diff <  0)) && return 0
    [[ $condition = '>=' ]] && ((diff <= 0)) && return 0
    return 1
}

Here is the test:

for tv1 in '*' 1.1.1 2.5.3 7.3.0 0.5.7 10.3.9 8.55.32 0.0.1; do
    for tv2 in 3.1.1 1.5.3 4.3.0 0.0.7 0.3.9 11.55.32 10.0.0 '*'; do
      for c in '=' '>' '<' '>=' '<=' '!='; do
        vercmp "$tv1" "$tv2" "$c" && printf '%s\n' "$tv1 $c $tv2 is true" || printf '%s\n' "$tv1 $c $tv2 is false"
      done
    done
done

A subset of the test output:

<snip>

* >= * is true
* <= * is true
* != * is true
1.1.1 = 3.1.1 is false
1.1.1 > 3.1.1 is false
1.1.1 < 3.1.1 is true
1.1.1 >= 3.1.1 is false
1.1.1 <= 3.1.1 is true
1.1.1 != 3.1.1 is true
1.1.1 = 1.5.3 is false
1.1.1 > 1.5.3 is false
1.1.1 < 1.5.3 is true
1.1.1 >= 1.5.3 is false
1.1.1 <= 1.5.3 is true
1.1.1 != 1.5.3 is true
1.1.1 = 4.3.0 is false
1.1.1 > 4.3.0 is false

<snip>
codeforester
  • 39,467
  • 16
  • 112
  • 140
5

Here's a pure Bash solution that supports revisions (e.g. '1.0-r1'), based on the answer posted by Dennis Williamson. It can easily be modified to support stuff like '-RC1' or extract the version from a more complex string by changing the regular expression.

For details regarding the implementation, please refer to in-code comments and/or enable the included debug code:

#!/bin/bash

# Compare two version strings [$1: version string 1 (v1), $2: version string 2 (v2)]
# Return values:
#   0: v1 == v2
#   1: v1 > v2
#   2: v1 < v2
# Based on: https://stackoverflow.com/a/4025065 by Dennis Williamson
function compare_versions() {

    # Trivial v1 == v2 test based on string comparison
    [[ "$1" == "$2" ]] && return 0

    # Local variables
    local regex="^(.*)-r([0-9]*)$" va1=() vr1=0 va2=() vr2=0 len i IFS="."

    # Split version strings into arrays, extract trailing revisions
    if [[ "$1" =~ ${regex} ]]; then
        va1=(${BASH_REMATCH[1]})
        [[ -n "${BASH_REMATCH[2]}" ]] && vr1=${BASH_REMATCH[2]}
    else
        va1=($1)
    fi
    if [[ "$2" =~ ${regex} ]]; then
        va2=(${BASH_REMATCH[1]})
        [[ -n "${BASH_REMATCH[2]}" ]] && vr2=${BASH_REMATCH[2]}
    else
        va2=($2)
    fi

    # Bring va1 and va2 to same length by filling empty fields with zeros
    (( ${#va1[@]} > ${#va2[@]} )) && len=${#va1[@]} || len=${#va2[@]}
    for ((i=0; i < len; ++i)); do
        [[ -z "${va1[i]}" ]] && va1[i]="0"
        [[ -z "${va2[i]}" ]] && va2[i]="0"
    done

    # Append revisions, increment length
    va1+=($vr1)
    va2+=($vr2)
    len=$((len+1))

    # *** DEBUG ***
    #echo "TEST: '${va1[@]} (?) ${va2[@]}'"

    # Compare version elements, check if v1 > v2 or v1 < v2
    for ((i=0; i < len; ++i)); do
        if (( 10#${va1[i]} > 10#${va2[i]} )); then
            return 1
        elif (( 10#${va1[i]} < 10#${va2[i]} )); then
            return 2
        fi
    done

    # All elements are equal, thus v1 == v2
    return 0
}


# ---------- everything below this line is just for testing ----------


# Test compare_versions [$1: version string 1, $2: version string 2, $3: expected result]
function test_compare_versions() {
    local op
    compare_versions "$1" "$2"
    case $? in
        0) op="==" ;;
        1) op=">" ;;
        2) op="<" ;;
    esac
    if [[ "$op" == "$3" ]]; then
        echo -e "\e[1;32mPASS: '$1 $op $2'\e[0m"
    else
        echo -e "\e[1;31mFAIL: '$1 $3 $2' (result: '$1 $op $2')\e[0m"
    fi
}

echo -e "\nThe following tests should pass:"
while read -r test; do
    test_compare_versions $test
done << EOF
1            1            ==
2.1          2.2          <
3.0.4.10     3.0.4.2      >
4.08         4.08.01      <
3.2.1.9.8144 3.2          >
3.2          3.2.1.9.8144 <
1.2          2.1          <
2.1          1.2          >
5.6.7        5.6.7        ==
1.01.1       1.1.1        ==
1.1.1        1.01.1       ==
1            1.0          ==
1.0          1            ==
1.0.2.0      1.0.2        ==
1..0         1.0          ==
1.0          1..0         ==
1.0-r1       1.0-r3       <
1.0-r9       2.0          <
3.0-r15      3.0-r9       >
...-r1       ...-r2       <
2.0-r1       1.9.8.21-r2  >
1.0          3.8.9.32-r   <
-r           -r3          <
-r3          -r           >
-r3          -r3          ==
-r           -r           ==
0.0-r2       0.0.0.0-r2   ==
1.0.0.0-r2   1.0-r2       ==
0.0.0.1-r7   -r9          >
0.0-r0       0            ==
1.002.0-r6   1.2.0-r7     <
001.001-r2   1.1-r2       ==
5.6.1-r0     5.6.1        ==
EOF

echo -e "\nThe following tests should fail:"
while read -r test; do
    test_compare_versions $test
done << EOF
1            1            >
3.0.5-r5     3..5-r5      >
4.9.21-r3    4.8.22-r9    <
1.0-r        1.0-r1       ==
-r           1.0-r        >
-r1          0.0-r1       <
-r2          0-r2         <
EOF
Fonic
  • 2,625
  • 23
  • 20
5

This is also a pure bash solution, as printf is a bash builtin.

function ver()
# Description: use for comparisons of version strings.
# $1  : a version string of form 1.2.3.4
# use: (( $(ver 1.2.3.4) >= $(ver 1.2.3.3) )) && echo "yes" || echo "no"
{
    printf "%02d%02d%02d%02d" ${1//./ }
}
Christopher
  • 51
  • 1
  • 2
  • 1
    Limited... Only works for pure numbers less than 100 with exactly 4 values. Nice try! – anthony Jun 01 '20 at 03:11
  • I like this one, but you can avoid 'invalid number' errors with `%02s`, and pad more for bigger values; I went for an impure variant that extends this to letters (which sort before numbers): alpha=a, beta=b, prefix a-z with ',' to sort before 0-9; [-.] separators; 0 pad: `local v=$(echo "$1"|sed -Ee 's/alpha/a/g;s/beta/b/g;s/([a-z]+)/,\1/g;s/-/./g;'); printf "%09s%09s%09s%09s%09s%09s%09s" ${v//./ }` – bazzargh Nov 03 '20 at 18:29
4

I'm using embedded Linux (Yocto) with BusyBox. BusyBox sort doesn't have a -V option (but BusyBox expr match can do regular expressions). So I needed a Bash version compare which worked with that constraint.

I've made the following (similar to Dennis Williamson's answer) to compare using a "natural sort" type of algorithm. It splits the string into numeric parts and non-numeric parts; it compares the numeric parts numerically (so 10 is greater than 9), and compares the non-numeric parts as a plain ASCII comparison.

ascii_frag() {
    expr match "$1" "\([^[:digit:]]*\)"
}

ascii_remainder() {
    expr match "$1" "[^[:digit:]]*\(.*\)"
}

numeric_frag() {
    expr match "$1" "\([[:digit:]]*\)"
}

numeric_remainder() {
    expr match "$1" "[[:digit:]]*\(.*\)"
}

vercomp_debug() {
    OUT="$1"
    #echo "${OUT}"
}

# return 1 for $1 > $2
# return 2 for $1 < $2
# return 0 for equal
vercomp() {
    local WORK1="$1"
    local WORK2="$2"
    local NUM1="", NUM2="", ASCII1="", ASCII2=""
    while true; do
        vercomp_debug "ASCII compare"
        ASCII1=`ascii_frag "${WORK1}"`
        ASCII2=`ascii_frag "${WORK2}"`
        WORK1=`ascii_remainder "${WORK1}"`
        WORK2=`ascii_remainder "${WORK2}"`
        vercomp_debug "\"${ASCII1}\" remainder \"${WORK1}\""
        vercomp_debug "\"${ASCII2}\" remainder \"${WORK2}\""

        if [ "${ASCII1}" \> "${ASCII2}" ]; then
            vercomp_debug "ascii ${ASCII1} > ${ASCII2}"
            return 1
        elif [ "${ASCII1}" \< "${ASCII2}" ]; then
            vercomp_debug "ascii ${ASCII1} < ${ASCII2}"
            return 2
        fi
        vercomp_debug "--------"

        vercomp_debug "Numeric compare"
        NUM1=`numeric_frag "${WORK1}"`
        NUM2=`numeric_frag "${WORK2}"`
        WORK1=`numeric_remainder "${WORK1}"`
        WORK2=`numeric_remainder "${WORK2}"`
        vercomp_debug "\"${NUM1}\" remainder \"${WORK1}\""
        vercomp_debug "\"${NUM2}\" remainder \"${WORK2}\""

        if [ -z "${NUM1}" -a -z "${NUM2}" ]; then
            vercomp_debug "blank 1 and blank 2 equal"
            return 0
        elif [ -z "${NUM1}" -a -n "${NUM2}" ]; then
            vercomp_debug "blank 1 less than non-blank 2"
            return 2
        elif [ -n "${NUM1}" -a -z "${NUM2}" ]; then
            vercomp_debug "non-blank 1 greater than blank 2"
            return 1
        fi

        if [ "${NUM1}" -gt "${NUM2}" ]; then
            vercomp_debug "num ${NUM1} > ${NUM2}"
            return 1
        elif [ "${NUM1}" -lt "${NUM2}" ]; then
            vercomp_debug "num ${NUM1} < ${NUM2}"
            return 2
        fi
        vercomp_debug "--------"
    done
}

It can compare more complicated version numbers such as

  • 1.2-r3 versus 1.2-r4
  • 1.2rc3 versus 1.2r4

Note that it doesn't return the same result for some of the corner-cases in Dennis Williamson's answer. In particular:

1            1.0          <
1.0          1            >
1.0.2.0      1.0.2        >
1..0         1.0          >
1.0          1..0         <

But those are corner cases, and I think the results are still reasonable.

Community
  • 1
  • 1
Craig McQueen
  • 41,871
  • 30
  • 130
  • 181
4

You all gave complicated solutions. Here's a simpler one.

function compare_versions {
    local a=${1%%.*} b=${2%%.*}
    [[ "10#${a:-0}" -gt "10#${b:-0}" ]] && return 1
    [[ "10#${a:-0}" -lt "10#${b:-0}" ]] && return 2
    a=${1:${#a} + 1} b=${2:${#b} + 1}
    [[ -z $a && -z $b ]] || compare_versions "$a" "$b"
}

Usage: compare_versions <ver_a> <ver_b>

Return code 1 means first version is greater than second, 2 means less, and 0 means both are equal.


Also a non-recursive version:

function compare_versions {
    local a=$1 b=$2 x y

    while [[ $a || $b ]]; do
        x=${a%%.*} y=${b%%.*}
        [[ "10#${x:-0}" -gt "10#${y:-0}" ]] && return 1
        [[ "10#${x:-0}" -lt "10#${y:-0}" ]] && return 2
        a=${a:${#x} + 1} b=${b:${#y} + 1}
    done

    return 0
}
konsolebox
  • 72,135
  • 12
  • 99
  • 105
2

For old version/busybox sort. Simple form provide roughly result and often works.

sort -n

This is escpecial useful on version which contains alpha symbols like

10.c.3
10.a.4
2.b.5
Daniel YC Lin
  • 15,050
  • 18
  • 63
  • 96
2

Here's a refinement of the top answer (Dennis's) that is more concise and uses a different return value scheme to make it easy to implement <= and >= with a single comparison. It also compares everything after the first character not in [0-9.] lexicographically, so 1.0rc1 < 1.0rc2.

# Compares two tuple-based, dot-delimited version numbers a and b (possibly
# with arbitrary string suffixes). Returns:
# 1 if a<b
# 2 if equal
# 3 if a>b
# Everything after the first character not in [0-9.] is compared
# lexicographically using ASCII ordering if the tuple-based versions are equal.
compare_versions() {
    if [[ $1 == "$2" ]]; then
        return 2
    fi
    local IFS=.
    local i a=(${1%%[^0-9.]*}) b=(${2%%[^0-9.]*})
    local arem=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
    for ((i=0; i<${#a[@]} || i<${#b[@]}; i++)); do
        if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
            return 1
        elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
            return 3
        fi
    done
    if [ "$arem" '<' "$brem" ]; then
        return 1
    elif [ "$arem" '>' "$brem" ]; then
        return 3
    fi
    return 2
}

To address @gammazero's comment, a longer version that (I think) is compatible with semantic versioning is this:

# Compares two dot-delimited decimal-element version numbers a and b that may
# also have arbitrary string suffixes. Compatible with semantic versioning, but
# not as strict: comparisons of non-semver strings may have unexpected
# behavior.
#
# Returns:
# 1 if a<b
# 2 if equal
# 3 if a>b
compare_versions() {
    local LC_ALL=C

    # Optimization
    if [[ $1 == "$2" ]]; then
        return 2
    fi

    # Compare numeric release versions. Supports an arbitrary number of numeric
    # elements (i.e., not just X.Y.Z) in which unspecified indices are regarded
    # as 0.
    local aver=${1%%[^0-9.]*} bver=${2%%[^0-9.]*}
    local arem=${1#$aver} brem=${2#$bver}
    local IFS=.
    local i a=($aver) b=($bver)
    for ((i=0; i<${#a[@]} || i<${#b[@]}; i++)); do
        if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
            return 1
        elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
            return 3
        fi
    done

    # Remove build metadata before remaining comparison
    arem=${arem%%+*}
    brem=${brem%%+*}

    # Prelease (w/remainder) always older than release (no remainder)
    if [ -n "$arem" -a -z "$brem" ]; then
        return 1
    elif [ -z "$arem" -a -n "$brem" ]; then
        return 3
    fi

    # Otherwise, split by periods and compare individual elements either
    # numerically or lexicographically
    local a=(${arem#-}) b=(${brem#-})
    for ((i=0; i<${#a[@]} && i<${#b[@]}; i++)); do
        local anns=${a[i]#${a[i]%%[^0-9]*}} bnns=${b[i]#${b[i]%%[^0-9]*}}
        if [ -z "$anns$bnns" ]; then
            # Both numeric
            if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
                return 1
            elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
                return 3
            fi
        elif [ -z "$anns" ]; then
            # Numeric comes before non-numeric
            return 1
        elif [ -z "$bnns" ]; then
            # Numeric comes before non-numeric
            return 3
        else
            # Compare lexicographically
            if [[ ${a[i]} < ${b[i]} ]]; then
                return 1
            elif [[ ${a[i]} > ${b[i]} ]]; then
                return 3
            fi
        fi
    done

    # Fewer elements is earlier
    if (( ${#a[@]} < ${#b[@]} )); then
        return 1
    elif (( ${#a[@]} > ${#b[@]} )); then
        return 3
    fi

    # Must be equal!
    return 2
}
Kyle Rose
  • 91
  • 2
  • 4
  • 2
    Here's an upvote because it's being used [here](https://github.com/discourse/discourse_docker/blob/755fff3554efadca975e5d7a6db3da135dd039e4/launcher#L93) – Codebling Feb 13 '18 at 16:11
  • There seems to be a problem when comparing prerelease and non-prerelease versions. The above indicates "1.0.0" < "1.0.0-alpha". However according to semver, "1.0.0-alpha" should be less than "1.0.0". – gammazero Aug 02 '21 at 14:26
1

How about this? Seems to work?

checkVersion() {
subVer1=$1
subVer2=$2

[ "$subVer1" == "$subVer2" ] && echo "Version is same"
echo "Version 1 is $subVer1"
testVer1=$subVer1
echo "Test version 1 is $testVer1"
x=0
while [[ $testVer1 != "" ]]
do
  ((x++))
  testVer1=`echo $subVer1|cut -d "." -f $x`
  echo "testVer1 now is $testVer1"
  testVer2=`echo $subVer2|cut -d "." -f $x`
  echo "testVer2 now is $testVer2"
  if [[ $testVer1 -gt $testVer2 ]]
  then
    echo "$ver1 is greater than $ver2"
    break
  elif [[ "$testVer2" -gt "$testVer1" ]]
  then
    echo "$ver2 is greater than $ver1"
    break
  fi
  echo "This is the sub verion for first value $testVer1"
  echo "This is the sub verion for second value $testVer2"
done
}

ver1=$1
ver2=$2
checkVersion "$ver1" "$ver2"
Artieman
  • 11
  • 1
1

Here is another pure bash solution without any external calls:

#!/bin/bash

function version_compare {

IFS='.' read -ra ver1 <<< "$1"
IFS='.' read -ra ver2 <<< "$2"

[[ ${#ver1[@]} -gt ${#ver2[@]} ]] && till=${#ver1[@]} || till=${#ver2[@]}

for ((i=0; i<${till}; i++)); do

    local num1; local num2;

    [[ -z ${ver1[i]} ]] && num1=0 || num1=${ver1[i]}
    [[ -z ${ver2[i]} ]] && num2=0 || num2=${ver2[i]}

    if [[ $num1 -gt $num2 ]]; then
        echo ">"; return 0
    elif
       [[ $num1 -lt $num2 ]]; then
        echo "<"; return 0
    fi
done

echo "="; return 0
}

echo "${1} $(version_compare "${1}" "${2}") ${2}"

And there is even more simple solution, if you are sure that the versions in question do not contain leading zeros after the first dot:

#!/bin/bash

function version_compare {

local ver1=${1//.}
local ver2=${2//.}


    if [[ $ver1 -gt $ver2 ]]; then
        echo ">"; return 0
    elif    
       [[ $ver1 -lt $ver2 ]]; then
        echo "<"; return 0
    fi 

echo "="; return 0
}

echo "${1} $(version_compare "${1}" "${2}") ${2}"

This will work for something like 1.2.3 vs 1.3.1 vs 0.9.7, but won't work with 1.2.3 vs 1.2.3.0 or 1.01.1 vs 1.1.1

1

I implemented yet another comparator function. This one had two specific requirements: (i) I didn't want the function to fail by using return 1 but echo instead; (ii) as we're retrieving versions from a git repository version "1.0" should be bigger than "1.0.2", meaning that "1.0" comes from trunk.

function version_compare {
  IFS="." read -a v_a <<< "$1"
  IFS="." read -a v_b <<< "$2"

  while [[ -n "$v_a" || -n "$v_b" ]]; do
    [[ -z "$v_a" || "$v_a" -gt "$v_b" ]] && echo 1 && return
    [[ -z "$v_b" || "$v_b" -gt "$v_a" ]] && echo -1 && return

    v_a=("${v_a[@]:1}")
    v_b=("${v_b[@]:1}")
  done

  echo 0
}

Feel free to comment and suggest improvements.

1

You can use version CLI to check version's constraints

$ version ">=1.0, <2.0" "1.7"
$ go version | version ">=1.9"

Bash script example:

#!/bin/bash

if `version -b ">=9.0.0" "$(gcc --version)"`; then
  echo "gcc version satisfies constraints >=9.0.0"
else
  echo "gcc version doesn't satisfies constraints >=9.0.0"
fi
Ivan Dyachenko
  • 1,348
  • 9
  • 16
1

Wow... this is way down the list of an old question, but I think this is a pretty elegant answer. First convert each dot-separated version into its own array, using shell parameter expansion (See Shell Parameter Expansion).

v1="05.2.3"     # some evil examples that work here
v2="7.001.0.0"

declare -a v1_array=(${v1//./ })
declare -a v2_array=(${v2//./ })

Now the two arrays have the version number as a numerical string in priority order. Lots of the above solutions take you from there, but it all derives from the observation that version string is just an integer with an arbitrary base. We can test finding the first unequal digit (like strcmp does for characters in a string).

compare_version() {
  declare -a v1_array=(${1//./ })
  declare -a v2_array=(${2//./ })

  while [[ -nz $v1_array ]] || [[ -nz $v2_array ]]; do
    let v1_val=${v1_array:-0}  # this will remove any leading zeros
    let v2_val=${v2_array:-0}
    let result=$((v1_val-v2_val))

    if (( result != 0 )); then
      echo $result
      return
    fi

    v1_array=("${v1_array[@]:1}") # trim off the first "digit". it doesn't help
    v2_array=("${v2_array[@]:1}")
  done

  # if we get here, both the arrays are empty and neither has been numerically
  # different, which is equivalent to the two versions being equal

  echo 0
  return
}

This echoes a negative number if the first version is less than the second, a zero if they're equal and a positive number if the first version is greater. Some output:

$ compare_version 1 1.2
-2
$ compare_version "05.1.3" "5.001.03.0.0.0.1"
-1
$ compare_version "05.1.3" "5.001.03.0.0.0"
0
$ compare_version "05.1.3" "5.001.03.0"
0
$ compare_version "05.1.3" "5.001.30.0"
-27
$ compare_version "05.2.3" "7.001.0.0"
-2
$ compare_version "05.1.3" "5.001.30.0"
-27
$ compare_version "7.001.0.0" "05.1.3"
2

Degenerate cases like, ".2" or "3.0." don't work (undefined results), and if non-numeric characters are present beside the '.' it might fail (haven't tested) but will certainly be undefined. So this should be paired with a sanitizing function or appropriate check for valid formatting. Also, I'm sure with some tweaking, this could be made more robust without too much extra baggage.

cycollins
  • 415
  • 4
  • 11
1
ver_cmp()
{
    local IFS=.
    local V1=($1) V2=($2) I
    for ((I=0 ; I<${#V1[*]} || I<${#V2[*]} ; I++)) ; do
        [[ ${V1[$I]:-0} -lt ${V2[$I]:-0} ]] && echo -1 && return
        [[ ${V1[$I]:-0} -gt ${V2[$I]:-0} ]] && echo 1 && return
    done
    echo 0
}

ver_eq()
{
    [[ $(ver_cmp "$1" "$2") -eq 0 ]]
}

ver_lt()
{
    [[ $(ver_cmp "$1" "$2") -eq -1 ]]
}

ver_gt()
{
    [[ $(ver_cmp "$1" "$2") -eq 1 ]]
}

ver_le()
{
    [[ ! $(ver_cmp "$1" "$2") -eq 1 ]]
}

ver_ge()
{
    [[ ! $(ver_cmp "$1" "$2") -eq -1 ]]
}

To test:

( ( while read V1 V2 ; do echo $V1 $(ver_cmp $V1 $V2) $V2 ; done ) <<EOF
1.2.3 2.2.3
2.2.3 2.2.2
3.10 3.2
2.2 2.2.1
3.1 3.1.0
EOF
) | sed 's/ -1 / < / ; s/ 0 / = / ; s/ 1 / > /' | column -t

1.2.3  <  2.2.3
2.2.3  >  2.2.2
3.10   >  3.2
2.2    <  2.2.1
3.1    =  3.1.0


ver_lt 10.1.2 10.1.20 && echo 'Your version is too old'

Your version is too old

btb91
  • 66
  • 2
1

When the Bash gets too complicated, just pipe it into python!

vercomp(){ echo "$1" "$2" | python3 -c "import re, sys; arr = lambda x: list(map(int, re.split('[^0-9]+', x))); x, y = map(arr, sys.stdin.read().split()); exit(not x >= y)"; }

Example comparing two version numbers:

vercomp 2.8 2.4.5 && echo ">=" || echo "<"

This python one liner compares the left version number to the right version number and exits 0 if the left version is equal or higher. It also handles versions like 2.4.5rc3

Broken down, this is the readable code:

import re, sys

# Convert a version string into a list "2.4.5" -> [2, 4, 5]
arr = lambda x: list(map(int, re.split('[^0-9]+', x)))

# Read the version numbers from stdin and apply the above function to them
x, y = map(arr, sys.stdin.read().split())

# Exit 0 if the left number is greater than the right
exit(not x >= y)
SurpriseDog
  • 462
  • 8
  • 18
  • 1
    I like this one because it's straight forward and py is on most systems. Thanks for sharing. PS> it would be helpful if it also stated if the package level were equal. – Mike Q Feb 21 '23 at 18:02
1

Instead of overcomplicating your life by writing long code, use something what already exists. Many times when bash is not enough python can help. And you still can call it from your bash script easily (bonus: variable substitution from bash to python):

VERSION1=1.2.3
VERSION2=1.2.4

cat <<EOF | python3 | grep -q True
from packaging import version
print(version.parse("$VERSION1") > version.parse("$VERSION2"))
EOF

if [ "$?" == 0 ];  then
   echo "$VERSION1 is greater than $VERSION2"
else
   echo "$VERSION2 is greater or equal than $VERSION1"
fi

More here: How do I compare version numbers in Python?

redseven
  • 849
  • 6
  • 11
  • Very nice suggestion, but `packaging` module isn't part of Python "stdlib", so one need to required additional steps to make sure it's installed. – mr0re1 Apr 24 '23 at 23:09
0

I came across and solved this problem, to add an additional (and shorter and simpler) answer...

First note, extended shell comparison failed as you may already know...

    if [[ 1.2.0 < 1.12.12 ]]; then echo true; else echo false; fi
    false

Using the sort -t'.'-g (or sort -V as mentioned by kanaka) to order versions and simple bash string comparison I found a solution. The input file contains versions in columns 3 and 4 which I want to compare. This iterates through the list identifying a match or if one is greater than the other. Hope this may still help anyone looking to do this using bash as simple as possible.

while read l
do
    #Field 3 contains version on left to compare (change -f3 to required column).
    kf=$(echo $l | cut -d ' ' -f3)
    #Field 4 contains version on right to compare (change -f4 to required column).
    mp=$(echo $l | cut -d ' ' -f4)

    echo 'kf = '$kf
    echo 'mp = '$mp

    #To compare versions m.m.m the two can be listed and sorted with a . separator and the greater version found.
    gv=$(echo -e $kf'\n'$mp | sort -t'.' -g | tail -n 1)

    if [ $kf = $mp ]; then 
        echo 'Match Found: '$l
    elif [ $kf = $gv ]; then
        echo 'Karaf feature file version is greater '$l
    elif [ $mp = $gv ]; then
        echo 'Maven pom file version is greater '$l
   else
       echo 'Comparison error '$l
   fi
done < features_and_pom_versions.tmp.txt

Thanks to Barry's blog for the sort idea... ref: http://bkhome.org/blog/?viewDetailed=02199

JStrahl
  • 444
  • 1
  • 8
  • 18
0
### the answer is does we second argument is higher
function _ver_higher {
        ver=`echo -ne "$1\n$2" |sort -Vr |head -n1`
        if [ "$2" == "$1" ]; then
                return 1
        elif [ "$2" == "$ver" ]; then
                return 0
        else
                return 1
        fi
}

if _ver_higher $1 $2; then
        echo higher
else
        echo same or less
fi

It's pretty simple and small.

erh
  • 231
  • 2
  • 3
  • This will break when there are backslashes in the versions, better replace `echo -ne "$1\n$2"` with `printf '%s\n ' "$1" "$2"`. Also it's better to use `$()` instead of the backtics. – phk Nov 05 '16 at 10:52
0

Thanks to Dennis's solution, we can extend it to allow comparison operators '>', '<', '=', '==', '<=', and '>='.

# compver ver1 '=|==|>|<|>=|<=' ver2
compver() { 
    local op
    vercomp $1 $3
    case $? in
        0) op='=';;
        1) op='>';;
        2) op='<';;
    esac
    [[ $2 == *$op* ]] && return 0 || return 1
}

We can then use comparison operators in the expressions like:

compver 1.7 '<=' 1.8
compver 1.7 '==' 1.7
compver 1.7 '=' 1.7

and test only the true/false of the result, like:

if compver $ver1 '>' $ver2; then
    echo "Newer"
fi
Luke Lee
  • 261
  • 3
  • 11
0

Here's another pure bash version, rather smaller than the accepted answer. It only checks whether a version is less than or equal to a "minimum version", and it will check alphanumeric sequences lexicographically, which often gives the wrong result ("snapshot" is not later than "release", to give a common example). It will work fine for major/minor.

is_number() {
    case "$BASH_VERSION" in
        3.1.*)
            PATTERN='\^\[0-9\]+\$'
            ;;
        *)
            PATTERN='^[0-9]+$'
            ;;
    esac

    [[ "$1" =~ $PATTERN ]]
}

min_version() {
    if [[ $# != 2 ]]
    then
        echo "Usage: min_version current minimum"
        return
    fi

    A="${1%%.*}"
    B="${2%%.*}"

    if [[ "$A" != "$1" && "$B" != "$2" && "$A" == "$B" ]]
    then
        min_version "${1#*.}" "${2#*.}"
    else
        if is_number "$A" && is_number "$B"
        then
            [[ "$A" -ge "$B" ]]
        else
            [[ ! "$A" < "$B" ]]
        fi
    fi
}
Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
0

Another approach(modified version of @joynes) that compares dotted versions as asked in the question
(i.e "1.2", "2.3.4", "1.0", "1.10.1", etc.).
The maximum number of positions has to be known in advance. The approach expects max 3 version positions.

expr $(printf "$1\n$2" | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -g | sed -n 2p) != $2

example usage:

expr $(printf "1.10.1\n1.7" | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -g | sed -n 2p) != "1.7"

returns: 1 since 1.10.1 is bigger than 1.7

expr $(printf "1.10.1\n1.11" | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -g | sed -n 2p) != "1.11"

returns: 0 since 1.10.1 is lower than 1.11

magiccrafter
  • 5,175
  • 1
  • 56
  • 50
0
function version_compare () {
  function sub_ver () {
    local len=${#1}
    temp=${1%%"."*} && indexOf=`echo ${1%%"."*} | echo ${#temp}`
    echo -e "${1:0:indexOf}"
  }
  function cut_dot () {
    local offset=${#1}
    local length=${#2}
    echo -e "${2:((++offset)):length}"
  }
  if [ -z "$1" ] || [ -z "$2" ]; then
    echo "=" && exit 0
  fi
  local v1=`echo -e "${1}" | tr -d '[[:space:]]'`
  local v2=`echo -e "${2}" | tr -d '[[:space:]]'`
  local v1_sub=`sub_ver $v1`
  local v2_sub=`sub_ver $v2`
  if (( v1_sub > v2_sub )); then
    echo ">"
  elif (( v1_sub < v2_sub )); then
    echo "<"
  else
    version_compare `cut_dot $v1_sub $v1` `cut_dot $v2_sub $v2`
  fi
}

### Usage:

version_compare "1.2.3" "1.2.4"
# Output: <

Credit goes to @Shellman

Xaqron
  • 29,931
  • 42
  • 140
  • 205
0

I didn't like any of these solutions because they're buggy, non-portable, etc.

My (current) effort to produce a better solution... src:version_compare(), tests

Sorta-copied/Pasted here...

Source:

##
# Compare two versions.
#
# **Usage:** version_compare version1 operator version2
#
# - operator:
#
#   + ``lesser_than``, ``-lt``, ``<``
#   + ``lesser_than_or_equal``, ``-le``, ``<=``
#   + ``greater_than``, ``-gt``, ``>``
#   + ``greater_than_or_equal``, ``-ge``, ``>=``
#   + ``equal``, ``-eq``, ``==``
#   + ``not_equal``, ``-ne``, ``!=``
#
# - version{1,2}: arbitrary version strings to compare
#
# **Version Format:** ``[0-9]+($VERSION_SEPARATOR[0-9]+)*`` (i.e. 1, 1.0, 90, 1.2.3.4)
#
# **Returns:** true if comparison statement is correct
##
version_compare() {
    _largest_version "$1" "$3"; _cmp="$(printf '%s' "$?")"

    # Check for valid responses or bail early
    case "$_cmp" in
        1|0|2) :;;
        *) _die "$_cmp" 'version comparison failed';;
    esac

    # The easy part
    case "$2" in
        'lesser_than'|'-lt'|'<')
            [ "$_cmp" = '2' ] && return 0
            ;;
        'lesser_or_equal'|'-le'|'<=')
            [ "$_cmp" = '0' ] && return 0
            [ "$_cmp" = '2' ] && return 0
            ;;
        'greater_than'|'-gt'|'>')
            [ "$_cmp" = '1' ] && return 0
            ;;
        'greater_or_equal'|'-ge'|'>=')
            [ "$_cmp" = '1' ] && return 0
            [ "$_cmp" = '0' ] && return 0
            ;;
        'equal'|'-eq'|'==')
            [ "$_cmp" = '0' ] && return 0
            ;;
        'not_equal'|'-ne'|'!=')
            [ "$_cmp" = '1' ] && return 0
            [ "$_cmp" = '2' ] && return 0
            ;;
        *) _die 7 'Unknown operatoration called for version_compare().';;
    esac
    return 1
}

##
# Print a formatted (critical) message and exit with status.
#
# **Usage:** _die [exit_status] message
#
# - exit_status: exit code to use with script termination (default: 1)
# - message: message to print before terminating script execution
##
_die() {
        # If first argument was an integer, use as exit_status
        if [ "$1" -eq "$1" ] 2>/dev/null; then
                _exit_status="$1"; shift
        else
                _exit_status=1
        fi

        printf '*** CRITICAL: %s ***\n' "$1"
        exit "$_exit_status"
}

##
# Compare two versions.
# Check if one version is larger/smaller/equal than/to another.
#
# **Usage:** _largest_version ver1 ver2
#
# Returns: ($1 > $2): 1 ; ($1 = $2): 0 ; ($1 < $2): 2
# [IOW- 1 = $1 is largest; 0 = neither ; 2 = $2 is largest]
##
_largest_version() (
    # Value used to separate version components
    VERSION_SEPARATOR="${VERSION_SEPARATOR:-.}"

    for _p in "$1" "$2"; do
        [ "$(printf %.1s "$_p")" = "$VERSION_SEPARATOR" ] &&
            _die 7 'invalid version pattern provided'
    done

    # Split versions on VER_SEP into int/sub
    _v="$1$2"
    _v1="$1"
    _s1="${1#*$VERSION_SEPARATOR}"
    if [ "$_v1" = "$_s1" ]; then
        _s1=''
        _m1="$_v1"
    else
        _m1="${1%%$VERSION_SEPARATOR*}"
    fi
    _v2="$2"
    _s2="${2#*$VERSION_SEPARATOR}"
    if [ "$_v2" = "$_s2" ]; then
        _s2=''
        _m2="$_v2"
    else
        _m2="${2%%$VERSION_SEPARATOR*}"
    fi

    # Both are equal
    [ "$_v1" = "$_v2" ] && return 0

    # Something is larger than nothing (30 < 30.0)
    if [ -n "$_v1" ] && [ ! -n "$_v2" ]; then
        return 1
    elif [ ! -n "$_v1" ] && [ -n "$_v2" ]; then
        return 2
    fi

    # Check for invalid
    case "$_m1$_m2" in
        *[!0-9]*)
            _die 7 'version_compare called with unsupported version type'
            ;;
    esac

    # If a ver_sep is present
    if [ "${_v#*$VERSION_SEPARATOR}" != "$_v" ]; then
        # Check for a larger "major" version number
        [ "$_m1" -lt "$_m2" ] && return 2
        [ "$_m1" -gt "$_m2" ] && return 1

        # Compare substring components
        _largest_version "$_s1" "$_s2"; return "$?"
    else
        # Only integers present; simple integer comparison
        [ "$_v1" -lt "$_v2" ] && return 2
        [ "$_v1" -gt "$_v2" ] && return 1
    fi
)

Tests:

# Simple test of all operators
( version_compare '1' 'lesser_than' '2' ); [ "$?" = '0' ] || return 1
( version_compare '2' 'equal' '2' ); [ "$?" = '0' ] || return 1
( version_compare '3' 'not_equal' '1' ); [ "$?" = '0' ] || return 1
( version_compare '2' 'greater_than' '1' ); [ "$?" = '0' ] || return 1
( version_compare '1' '-lt' '2' ); [ "$?" = '0' ] || return 1
( version_compare '2' '-eq' '2' ); [ "$?" = '0' ] || return 1
( version_compare '3' '-ne' '1' ); [ "$?" = '0' ] || return 1
( version_compare '2' '-gt' '1' ); [ "$?" = '0' ] || return 1

# Semver test of primary operators (expect true)
( version_compare '7.0.1' '-lt' '7.0.2' ); [ "$?" = '0' ] || return 1
( version_compare '7.0.2' '-eq' '7.0.2' ); [ "$?" = '0' ] || return 1
( version_compare '3.0.2' '-ne' '2.0.7' ); [ "$?" = '0' ] || return 1
( version_compare '7.0.2' '-gt' '7.0.1' ); [ "$?" = '0' ] || return 1

# Semver test of primary operators (expect false)
( version_compare '7.0.2' '-lt' '7.0.1' ); [ "$?" = '1' ] || return 1
( version_compare '3.0.2' '-eq' '2.0.7' ); [ "$?" = '1' ] || return 1
( version_compare '7.0.2' '-ne' '7.0.2' ); [ "$?" = '1' ] || return 1
( version_compare '7.0.1' '-gt' '7.0.2' ); [ "$?" = '1' ] || return 1

# Mismatched version strings (expect true)
( version_compare '7' '-lt' '7.1' ); [ "$?" = '0' ] || return 1
( version_compare '3' '-ne' '7.0.0' ); [ "$?" = '0' ] || return 1
( version_compare '7.0.1' '-gt' '7' ); [ "$?" = '0' ] || return 1

# Mismatched version strings (expect false)
( version_compare '7.0.0' '-eq' '7.0' ); [ "$?" = '1' ] || return 1

# Invalid operation supplied
( version_compare '2' '-inv' '1' >/dev/null ); [ "$?" = '7' ] || return 1

# Invalid version formats
( version_compare '1..0' '==' '1.0' >/dev/null ); [ "$?" = '7' ] || return 1
( version_compare '1.0' '==' '1..0' >/dev/null ); [ "$?" = '7' ] || return 1
( version_compare '1.0' '==' '1.0b7' >/dev/null ); [ "$?" = '7' ] || return 1
( version_compare '1.0a' '==' '1.0' >/dev/null ); [ "$?" = '7' ] || return 1

# "how does that handle comparing 10.0.0 (not a number) to 2.0 (a number)?"
( version_compare '10.0.0' '-lt' '2.0' ); [ "$?" = '1' ] || return 1
( version_compare '10.0' '-gt' '2.0.0' ); [ "$?" = '0' ] || return 1

# not less/greater-than... but equal
( version_compare '7' '-lt' '7' ); [ "$?" = '1' ] || return 1
( version_compare '7' '-gt' '7' ); [ "$?" = '1' ] || return 1

# String vs. numerical comparison
( version_compare '1.18.1' '-gt' '1.8.1' ); [ "$?" = '0' ] || return 1


# Random tests found on the internet
( version_compare '1' '==' '1' ); [ "$?" = '0' ] || return 1
( version_compare '2.1' '<' '2.2' ); [ "$?" = '0' ] || return 1
( version_compare '3.0.4.10' '>' '3.0.4.2' ); [ "$?" = '0' ] || return 1
( version_compare '4.08' '<' '4.08.01' ); [ "$?" = '0' ] || return 1
( version_compare '3.2.1.9.8144' '>' '3.2' ); [ "$?" = '0' ] || return 1
( version_compare '3.2' '<' '3.2.1.9.8144' ); [ "$?" = '0' ] || return 1
( version_compare '1.2' '<' '2.1' ); [ "$?" = '0' ] || return 1
( version_compare '2.1' '>' '1.2' ); [ "$?" = '0' ] || return 1
( version_compare '5.6.7' '==' '5.6.7' ); [ "$?" = '0' ] || return 1
( version_compare '1.01.1' '==' '1.1.1' ); [ "$?" = '0' ] || return 1
( version_compare '1.1.1' '==' '1.01.1' ); [ "$?" = '0' ] || return 1
( version_compare '1' '!=' '1.0' ); [ "$?" = '0' ] || return 1
( version_compare '1.0.0' '!=' '1.0' ); [ "$?" = '0' ] || return 1
MTeck
  • 1,642
  • 3
  • 16
  • 34
0

A useful trick here is string indexing.

$ echo "${BASH_VERSION}"
4.4.23(1)-release

$ echo "${BASH_VERSION:0:1}"
4
mikeLundquist
  • 769
  • 1
  • 12
  • 26
  • How is that useful? You need to know the length to get the correct number... – Alexis Wilke Jan 04 '21 at 18:47
  • @AlexisWilke The example I left shows getting the major version of bash which you can compare using `[ 4 -eq ${BASH_VERSION:0:1} ] && echo hi` which only prints hi if your bash version is 4. The question asked how to compare versions, which this does in a simple way albeit incomplete way. – mikeLundquist Jan 04 '21 at 19:01
  • What is the version was `41.4.23`, your `${BASH_VERSION:0:1}` would return `4`, not `41`... – Alexis Wilke Jan 04 '21 at 22:53
  • `${BASH_VERSION:0:2}` would return 41 but I haven't seen too many projects with 41 major versions. Again, this is just supposed to be the simplest answer to a subset of the question which was useful to me and may be to others. – mikeLundquist Jan 05 '21 at 14:52
  • `${BASH_VERSINFO[@]}` contains an array with the parts, no need to string-split. E.g. `echo "${BASH_VERSINFO[0]}"` would print `4` in your case. – amphetamachine Nov 16 '22 at 16:57
0

I use a function that normalizes the numbers and than compare them.

The for loop is necessary to convert octal numbers in version strings to decimal, e.g. 1.08 → 1 8, 1.0030 → 1 30, 2021-02-03 → 2021 2 3...

(Tested with bash 5.0.17

#!/usr/bin/env bash

v() {
  printf "%04d%04d%04d%04d%04d" $(for i in ${1//[^0-9]/ }; do printf "%d " $((10#$i)); done)
}

while read -r test; do
  set -- $test
  printf "$test    "
  eval "if [[ $(v $1) $3 $(v $2) ]] ; then echo true; else echo false; fi"
done << EOF
1              1                   ==
2.1            2.2                  <
3.0.4.10       3.0.4.2              >
4.08           4.08.01              <
3.2.1.9.8144   3.2                  >
3.2            3.2.1.9.8144         <
1.2            2.1                  <
2.1            1.2                  >
5.6.7          5.6.7               ==
1.01.1         1.1.1               ==
1.1.1          1.01.1              ==
1              1.0                 ==
1.0            1                   ==
1.0.2.0        1.0.2               ==
1..0           1.0                 ==
1.0            1..0                ==
1              1                    >
1.2.3~rc2      1.2.3~rc4            >
1.2.3~rc2      1.2.3~rc4           ==
1.2.3~rc2      1.2.3~rc4            <
1.2.3~rc2      1.2.3~rc4           !=
1.2.3~rc2      1.2.3+rc4            <
2021-11-23-rc1 2021-11-23-rc1.1     <
2021-11-23-rc1 2021-11-23-rc1-rf1   <
2021-01-03-rc1 2021-01-04           <
5.0.17(1)-release 5.0.17(2)-release <
EOF

result:

1              1                   ==    true
2.1            2.2                  <    true
3.0.4.10       3.0.4.2              >    true
4.08           4.08.01              <    true
3.2.1.9.8144   3.2                  >    true
3.2            3.2.1.9.8144         <    true
1.2            2.1                  <    true
2.1            1.2                  >    true
5.6.7          5.6.7               ==    true
1.01.1         1.1.1               ==    true
1.1.1          1.01.1              ==    true
1              1.0                 ==    true
1.0            1                   ==    true
1.0.2.0        1.0.2               ==    true
1..0           1.0                 ==    true
1.0            1..0                ==    true
1              1                    >    false
1.2.3~rc2      1.2.3~rc4            >    false
1.2.3~rc2      1.2.3~rc4           ==    false
1.2.3~rc2      1.2.3~rc4            <    true
1.2.3~rc2      1.2.3~rc4           !=    true
1.2.3~rc2      1.2.3+rc4            <    true
2021-11-23-rc1 2021-11-23-rc1.1     <    true
2021-11-23-rc1 2021-11-23-rc1-rf1   <    true
2021-01-03-rc1 2021-01-04           <    true
5.0.17(1)-release 5.0.17(2)-release <    true
Reini
  • 51
  • 3
0

My two cents:

vercomp () {
    if [[ "${1}" == "${2}" ]]; then
        echo '0'
        return
    fi
    echo "${1}" | sed 's/\([0-9]\+\)\./\1\n/g' | {
        _RES_=-1
        for _VB_ in $(echo "${2}" | sed 's/\([0-9]\+\)\./\1\n/g'); do
            if ! read -r _VA_ || [[ "${_VB_}" -gt "${_VA_}" ]]; then
                _RES_=1
                break
            fi
        done
        read -r _VA_ && echo '-1' || echo "${_RES_}"
    }
}

Syntax:

vercomp VERSION_A VERSION_B

Prints:

  • -1 if VERSION_A is more recent
  • 0 if the two versions are equal
  • 1 if VERSION_B is more recent
madmurphy
  • 1,451
  • 11
  • 20
0

I hope this is useful to someone (use with awk)

  #!/bin/bash

  available_version=1.2.3 # or online version
  this_version=1.2.1

  if [[ "ok" == "$(echo | awk "(${available_version} > ${this_version}) { print \"ok\"; }")" ]]; then
    echo "Notice, new version is available"
  elif [[ "ok" == "$(echo | awk "(${available_version} == ${this_version}) { print \"ok\"; }")" ]]; then
    echo "OK, version is up to date"
  else
    echo "Warning, the current version is ahead of the online version!"
  fi
MaXi32
  • 632
  • 9
  • 26