Author Topic: bash, how to compare the versions of two items?  (Read 3674 times)

0 Members and 2 Guests are viewing this topic.

Offline DiTBhoTopic starter

  • Super Contributor
  • ***
  • Posts: 5083
  • Country: gb
bash, how to compare the versions of two items?
« on: April 24, 2025, 12:14:50 pm »
Code: [Select]
   cmp1=$(echo "$binutils_ver >= $binutils_min" | bc -l)
   cmp2=$(echo "$binutils_ver <= $binutils_max" | bc -l)

Code: [Select]
echo "2.40 >= 2.30" | bc -l

This always works, and returns { 1, 0 } -> { True, False }

However, it fails when the version contains a subversion
e.g.
2.40.1 vs 2.40.5 ------> fails!
Code: [Select]
(standard_in) 1: syntax error
(standard_in) 1: syntax error

Is there a better way or tool/alternative to sys-devel/bc to solve this problem?
The opposite of courage is not cowardice, it is conformity. Even a dead fish can go with the flow
 

Offline tunk

  • Super Contributor
  • ***
  • Posts: 1409
  • Country: no
Re: bash, how to compare the versions of two items?
« Reply #1 on: April 24, 2025, 12:42:51 pm »
Wouldn't really know, maybe somehow convert the 2.40.1 text string to e.g. 2.40001?
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 17773
  • Country: fr
Re: bash, how to compare the versions of two items?
« Reply #2 on: April 24, 2025, 01:06:56 pm »
Ah yeah, of course, it compares numbers. 2.40.1 is not a number.
Problem is,
- lexicographic comparison as strings won't work either for comparing versions.
- just removing the dots before comparing won't work either.

I found this, which handles version components up to 3 digits each (but accomodates fewer) and gives a single string that can be lexicographically compared directly in bash (with bash comparisons such as '-ge'):

echo "2.40.1" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'
2040001000

echo "2.40.10" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'
2040010000

echo "2.40" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'
2040000000

You can make this a bash function so that it's easy to reuse:
Code: [Select]
function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }

You can then compare two version strings like so:
Code: [Select]
if [ $(version $ver1) -ge $(version $ver2) ]; then
   
fi
« Last Edit: April 24, 2025, 01:08:57 pm by SiliconWizard »
 
The following users thanked this post: DiTBho

Offline madires

  • Super Contributor
  • ***
  • Posts: 9166
  • Country: de
  • A qualified hobbyist ;)
Re: bash, how to compare the versions of two items?
« Reply #3 on: April 24, 2025, 01:24:17 pm »
I'd suggest to write a small awk script to extract the numbers between the dots and then compare the numbers tier-wise (major, minor, revision, sub-revision, ...).
 

Offline DiTBhoTopic starter

  • Super Contributor
  • ***
  • Posts: 5083
  • Country: gb
Re: bash, how to compare the versions of two items?
« Reply #4 on: April 24, 2025, 02:53:59 pm »
the comparison is needed in a big building tool I wrote in bash to ensure that various legacy Linux builds are compiled with the right tools, and to give an immediate indication of which versions of the tools have been successfully tested
Code: [Select]
toolchain(armv5tel-softfloat-linux-gnueabi:2.40.0/12)
-----------------------------------------------
[!] panic
    module=profile/do/check_gcc
    my_fid=check_gcc
    reason=found gcc-v12, needed gcc-v{ 6.0.0 .. 6.6.0 }

At the moment I am removing the right dot, so "6.6.0" becomes "6.6", then is compared by bc.
Which is "ok", but not optimal.

I will in the near future rewrite the whole builder in C/89.
The opposite of courage is not cowardice, it is conformity. Even a dead fish can go with the flow
 

Offline DiTBhoTopic starter

  • Super Contributor
  • ***
  • Posts: 5083
  • Country: gb
Re: bash, how to compare the versions of two items?
« Reply #5 on: April 24, 2025, 03:15:36 pm »
@SiliconWizard
added your method  :-+
The opposite of courage is not cowardice, it is conformity. Even a dead fish can go with the flow
 

Online ledtester

  • Super Contributor
  • ***
  • Posts: 4115
  • Country: us
Re: bash, how to compare the versions of two items?
« Reply #6 on: April 24, 2025, 03:20:12 pm »
 
The following users thanked this post: DiTBho

Online radiolistener

  • Super Contributor
  • ***
  • Posts: 5730
  • Country: Earth
Re: bash, how to compare the versions of two items?
« Reply #7 on: April 25, 2025, 06:21:32 pm »
Code: [Select]
echo "2.40 >= 2.30" | bc -l

This always works, and returns { 1, 0 } -> { True, False }

interesting, but it returns incorrect result in this case:
Code: [Select]
$ echo "2.40 >= 2.5" | bc -l
0

it shows false
But 40 >= 5 should be true
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 17773
  • Country: fr
Re: bash, how to compare the versions of two items?
« Reply #8 on: April 25, 2025, 08:25:25 pm »
Code: [Select]
echo "2.40 >= 2.30" | bc -l

This always works, and returns { 1, 0 } -> { True, False }

interesting, but it returns incorrect result in this case:
Code: [Select]
$ echo "2.40 >= 2.5" | bc -l
0

it shows false
But 40 >= 5 should be true

No, bc is just a calculator comparing numbers.
2.40 is not greater than 2.5.
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: bash, how to compare the versions of two items?
« Reply #9 on: April 25, 2025, 10:10:51 pm »
If you have a GNU-compatible standard C library, just use strverscmp().  For example:
Code: [Select]
// SPDX-License-Identifier: CC0-1.0
// gcc -Wall -O2 verscmp.c -o verscmp

#define  _GNU_SOURCE
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

int main(int argc, char *argv[]) {
    const char *const arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "this";

    if (argc != 3 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);
        fprintf(stderr, "       %s BEFORE AFTER\n", arg0);
        fprintf(stderr, "\n");
        fprintf(stderr, "This returns\n");
        fprintf(stderr, "    0  if BEFORE < AFTER,\n");
        fprintf(stderr, "    1  if BEFORE = AFTER,\n");
        fprintf(stderr, "    2  if BEFORE > AFTER, or\n");
        fprintf(stderr, "    3  if the strings cannot be compared.\n");
        fprintf(stderr, "\n");
        return 3;
    }

    if (!argv[1] || !argv[2])
        return 3;

    int  result = strverscmp(argv[1], argv[2]);
    return (result < 0) ? 0 : (result > 0) ? 2 : 1;
}
In Bash, the exit status of the last executed command is $?.  Compiling the above to verscmp:

    ./verscmp binutils-2.23.20 binutils-2.23
yields $? == 2 (binutils-2.23.20 > binutils-2.23),

    ./verscmp binutils-2.23 binutils-2.23.0
yields $? == 0 (binutils-2.23 < binutils-2.23.0),

    ./verscmp binutils-2.23.1 binutils-2.23.0
yields $? == 2 (binutils-2.23.1 > binutils-2.23.0),

    ./verscmp binutils-2.23.9 binutils-2.23.10
yields $? == 0 (binutils-2.23.9 > binutils-2.23.10), and

    ./verscmp binutils-2.23.13a binutils-2.23.13b
yields $? == 0 (binutils-2.23.13a < binutils-2.23.13b).  The return values change correspondingly if you swap the version strings.

In other words, it tends to do the right thing.  It is what Linux coreutils' ls -v uses, too.

To reimplement the same in your own non-GNU code, start by checking the length of the non-numeric prefix left in both strings.  Compare these as strings as usual, from left to right.  Whenever a character comparison is not equal, you have a result and can return the result of the comparison immediately.  Next, you check the length of the numeric prefixes left in both strings.  Compare these as strings from right to left; this is the same as comparing their decimal integer values, without parsing what that value is (and therefore also not limiting to any specific integer size).  Repeat until both strings have nothing left; the two strings are then equal.  This does mean that 010 < 10 < 11, but if you want 010 == 10, you can do that by special-casing the zero-prefix case (i.e., the right-to-left comparison ends at the start of one string, at either the start of the other string also or with only zeros left).
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 8349
  • Country: fi
    • My home page and email address
Re: bash, how to compare the versions of two items?
« Reply #10 on: April 25, 2025, 10:21:22 pm »
coreutils' sort -V also uses strverscmp().  So, if you have $VERS1 and $VERS2, then
    VERS="$(printf '%s\n' "$VERS1" "$VERS2" | sort -V | tail -1)"
gives you the latter ("bigger") version in $VERS.

You can then follow this up with
    [[ "$VERS" = "$VERS1" ]]
and so on.
 

Online radiolistener

  • Super Contributor
  • ***
  • Posts: 5730
  • Country: Earth
Re: bash, how to compare the versions of two items?
« Reply #11 on: April 25, 2025, 11:45:54 pm »
2.40 is not greater than 2.5.

While numeric comparison correctly evaluates 2.40 > 2.5 as false, this behavior is inappropriate for version comparison, where 2.40 is considered greater than 2.5. Relying on standard numeric comparison can therefore produce incorrect results when dealing with versions. This is a common pitfall, especially when attempting to sort versioned strings accurately.

To work around this, you can format version numbers by padding components with leading zeros to a fixed width that is unlikely to be exceeded - for example, using 2.005 instead of 2.5. However, this approach breaks down once you need to compare versions like 2.1005 and 2.005, as the comparison becomes incorrect again

In many cases, it's not feasible to reformat version numbers into a consistent, comparison-friendly format. More often, you're required to work with existing version strings that cannot be altered.
« Last Edit: April 25, 2025, 11:55:47 pm by radiolistener »
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 17773
  • Country: fr
Re: bash, how to compare the versions of two items?
« Reply #12 on: April 26, 2025, 01:30:37 am »
2.40 is not greater than 2.5.

While numeric comparison correctly evaluates 2.40 > 2.5 as false, this behavior is inappropriate for version comparison, where 2.40 is considered greater than 2.5. Relying on standard numeric comparison can therefore produce incorrect results when dealing with versions. This is a common pitfall, especially when attempting to sort versioned strings accurately.

To work around this, you can format version numbers by padding components with leading zeros to a fixed width that is unlikely to be exceeded - for example, using 2.005 instead of 2.5. However, this approach breaks down once you need to compare versions like 2.1005 and 2.005, as the comparison becomes incorrect again

In many cases, it's not feasible to reformat version numbers into a consistent, comparison-friendly format. More often, you're required to work with existing version strings that cannot be altered.

Have you read any of the posts in this thread? :horse:
 

Offline DiTBhoTopic starter

  • Super Contributor
  • ***
  • Posts: 5083
  • Country: gb
Re: bash, how to compare the versions of two items?
« Reply #13 on: April 27, 2025, 12:03:30 pm »
Next issue: what with version strings like "1.5.5-r1" ?
Revision
The opposite of courage is not cowardice, it is conformity. Even a dead fish can go with the flow
 

Offline 5U4GB

  • Super Contributor
  • ***
  • Posts: 1719
  • Country: au
Re: bash, how to compare the versions of two items?
« Reply #14 on: April 27, 2025, 03:42:05 pm »
This is tricky because you need to handle special cases like one- vs. two-digit version numbers, periods, sub-versions, etc.  For example if you're checking for a version of gcc or clang via "-dumpversion", which seems straightforward enough, then you need to deal with the fact that it lists major-version releases as a single-digit number, '6' rather than '6.0', and the major version itself may have one or two digits, so if you find a single-digit version you have to add a trailing zero to the string (first case), otherwise use the first two or three digits depending on what the string starts with.  However with a major-version release >= 10 you get the same problem as with single-digit major-versons so you also add a trailing zero to two-digit versions which will be removed by the 3-digit cut if the version is xyz already but not if it's xy0-added (second case).

So the resulting code, with $GCC_VERSION (or CLANG_VERSION) being the -dumpversion output, is:

case $GCC_VERSION in
   [0-9])
      GCC_VERSION="${GCC_VERSION}0" ;;
   [0-9][0-9]*)
      GCC_VERSION="$(echo ${GCC_VERSION}0 | tr -d  '.' | cut -c 1-3)" ;;
   *)
      GCC_VERSION="$(echo $GCC_VERSION | tr -d  '.' | cut -c 1-2)" ;;
esac
 

Online SiliconWizard

  • Super Contributor
  • ***
  • Posts: 17773
  • Country: fr
Re: bash, how to compare the versions of two items?
« Reply #15 on: April 27, 2025, 05:27:01 pm »
What about 1.5.5.0-chicago-rc1b ?
 :P
 

Offline Whales

  • Super Contributor
  • ***
  • Posts: 2682
  • Country: au
    • Halestrom
Re: bash, how to compare the versions of two items?
« Reply #16 on: April 27, 2025, 08:41:33 pm »
Had a tangentially similar problem sorting kernels.  6.10 > 6.9

Code: [Select]
for vmlinuz in $(echo vmlinuz-* | sort -t. -k 1,1nr -k 2,2nr -k 3,3nr -k 4,4nr)
do
  echo $vmlinuz
done

Output example:
Code: [Select]
vmlinuz-6.12.23_1
vmlinuz-6.12.21_1
vmlinuz-6.12.19_1
vmlinuz-6.6.87_1
vmlinuz-6.6.85_1
 

Offline DiTBhoTopic starter

  • Super Contributor
  • ***
  • Posts: 5083
  • Country: gb
Re: bash, how to compare the versions of two items?
« Reply #17 on: April 28, 2025, 05:11:42 am »
Release Candidate
Revision
The opposite of courage is not cowardice, it is conformity. Even a dead fish can go with the flow
 

Offline cdw

  • Contributor
  • Posts: 12
  • Country: gb
Re: bash, how to compare the versions of two items?
« Reply #18 on: April 28, 2025, 04:48:25 pm »
If you're looking for a shell version and don't mind depending on GNU coreutils for a full-featured sort(1), you could use sort -V for an easy life. For example,
Code: [Select]
versle() {
  [ "$(printf '%s\n' "$1" "$2" | sort -V | head -n 1)" = "$1" ]
}

implements a less-than-or-equal operation on version numbers:

Code: [Select]
# versle 1.2.3 1.2.4; echo $?
0
# versle 1.2.4 1.2.4; echo $?
0
# versle 1.2.4 1.2.3; echo $?
1

Edit: Ah, I see @Nominal Animal has beaten me to exactly this suggestion! Sorry, didn't spot that when I scanned through the first time.

Alternatively just recurse using -le comparisons on parameter expansions "${1%%.*}", chomping and recursing if equal using "${1#*.}" etc. if you'd prefer not to fork and exec. This will need a little care with the base case that doesn't contain '.' and you'll get errors for non-numeric versions unless you handle them specially.

« Last Edit: April 28, 2025, 04:55:28 pm by cdw »
 
The following users thanked this post: Nominal Animal


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf