bash, how to compare the versions of two items? - Page 1
Forum section: electronics / general

author: DiTBho
author_rank: Super Contributor
timestamp: April 24, 2025, 12:14:50 pm
content: Code:cmp1=$(echo "$binutils_ver >= $binutils_min" | bc -l)cmp2=$(echo "$binutils_ver <= $binutils_max" | bc -l)Code:echo "2.40 >= 2.30" | bc -lThis always works, and returns { 1, 0 } -> { True, False }However, it fails when the version contains a subversione.g.2.40.1 vs 2.40.5 ------> fails!Code:(standard_in) 1: syntax error(standard_in) 1: syntax errorIs there a better way or tool/alternative tosys-devel/bcto solve this problem?

author: tunk
author_rank: Super Contributor
timestamp: April 24, 2025, 12:42:51 pm
content: Wouldn't really know, maybe somehow convert the 2.40.1 text string to e.g. 2.40001?

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 24, 2025, 01:06:56 pm
content: 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); }'2040001000echo "2.40.10" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'2040010000echo "2.40" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'2040000000You can make this a bash function so that it's easy to reuse:Code: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:if [ $(version $ver1) -ge $(version $ver2) ]; thenfi

author: madires
author_rank: Super Contributor
timestamp: April 24, 2025, 01:24:17 pm
content: 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, ...).

author: DiTBho
author_rank: Super Contributor
timestamp: April 24, 2025, 02:53:59 pm
content: 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 testedCode:toolchain(armv5tel-softfloat-linux-gnueabi:2.40.0/12)-----------------------------------------------[!] panicmodule=profile/do/check_gccmy_fid=check_gccreason=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.

author: DiTBho
author_rank: Super Contributor
timestamp: April 24, 2025, 03:15:36 pm
content: @SiliconWizardadded your method

author: ledtester
author_rank: Super Contributor
timestamp: April 24, 2025, 03:20:12 pm
content: Both Perl and Python have modules dedicated to comparing version strings:Perl:https://perlmaven.com/how-to-compare-version-numbers-in-perl-and-for-cpan-moduleshttps://metacpan.org/pod/Version::ComparePython:https://stackoverflow.com/questions/11887762/how-do-i-compare-version-numbers-in-pythonYou could execute one of those routines from bash.

author: radiolistener
author_rank: Super Contributor
timestamp: April 25, 2025, 06:21:32 pm
content: Quote from: DiTBho on April 24, 2025, 12:14:50 pminteresting, but it returns incorrect result in this case:Code:$ echo "2.40 >= 2.5" | bc -l0it shows falseBut 40 >= 5 should be true

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 25, 2025, 08:25:25 pm
content: Quote from: radiolistener on April 25, 2025, 06:21:32 pmNo, bc is just a calculator comparing numbers.2.40 is not greater than 2.5.

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 25, 2025, 10:10:51 pm
content: If you have a GNU-compatible standard C library, just usestrverscmp(). For example:Code:// 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 && argv) ? argv : "this";if (argc != 3 || !strcmp(argv, "-h") || !strcmp(argv, "--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 || !argv)return 3;int result = strverscmp(argv, argv);return (result < 0) ? 0 : (result > 0) ? 2 : 1;}In Bash, the exit status of the last executed command is$?. Compiling the above toverscmp:./verscmp binutils-2.23.20 binutils-2.23yields$? == 2(binutils-2.23.20 > binutils-2.23),./verscmp binutils-2.23 binutils-2.23.0yields$? == 0(binutils-2.23 < binutils-2.23.0),./verscmp binutils-2.23.1 binutils-2.23.0yields$? == 2(binutils-2.23.1 > binutils-2.23.0),./verscmp binutils-2.23.9 binutils-2.23.10yields$? == 0(binutils-2.23.9 > binutils-2.23.10), and./verscmp binutils-2.23.13a binutils-2.23.13byields$? == 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 -vuses, 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 stringsfrom 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).

author: Nominal Animal
author_rank: Super Contributor
timestamp: April 25, 2025, 10:21:22 pm
content: coreutils'sort -Valso uses strverscmp(). So, if you have$VERS1and$VERS2, thenVERS="$(printf '%s\n' "$VERS1" "$VERS2" | sort -V | tail -1)"gives you thelatter("bigger") version in$VERS.You can then follow this up with[[ "$VERS" = "$VERS1" ]]and so on.

author: radiolistener
author_rank: Super Contributor
timestamp: April 25, 2025, 11:45:54 pm
content: Quote from: SiliconWizard on April 25, 2025, 08:25:25 pmWhile 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 againIn 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.

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 26, 2025, 01:30:37 am
content: Quote from: radiolistener on April 25, 2025, 11:45:54 pmHave you read any of the posts in this thread?

author: DiTBho
author_rank: Super Contributor
timestamp: April 27, 2025, 12:03:30 pm
content: Next issue: what with version strings like "1.5.5-r1" ?Revision

author: 5U4GB
author_rank: Super Contributor
timestamp: April 27, 2025, 03:42:05 pm
content: 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

author: SiliconWizard
author_rank: Super Contributor
timestamp: April 27, 2025, 05:27:01 pm
content: What about 1.5.5.0-chicago-rc1b ?

author: Whales
author_rank: Super Contributor
timestamp: April 27, 2025, 08:41:33 pm
content: Had a tangentially similar problem sorting kernels. 6.10 > 6.9Code:for vmlinuz in $(echo vmlinuz-* | sort -t. -k 1,1nr -k 2,2nr -k 3,3nr -k 4,4nr)doecho $vmlinuzdoneOutput example:Code:vmlinuz-6.12.23_1vmlinuz-6.12.21_1vmlinuz-6.12.19_1vmlinuz-6.6.87_1vmlinuz-6.6.85_1

author: DiTBho
author_rank: Super Contributor
timestamp: April 28, 2025, 05:11:42 am
content: Release CandidateRevision

author: cdw
author_rank: Contributor
timestamp: April 28, 2025, 04:48:25 pm
content: 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:versle() {[ "$(printf '%s\n' "$1" "$2" | sort -V | head -n 1)" = "$1" ]}implements a less-than-or-equal operation on version numbers:Code:# 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 $?1Edit: 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.