How to Compare Two Strings in Dot Separated Version Format in Bash

How to compare two strings in dot separated version format in Bash?

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'

How to compare two strings in dot separated version format in Bash?

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'

Comparing two version numbers in a shell script

The question says that ExpecV>=CurrV should be treated as success, but that does not make much sense (current version older than the expected one probably breaks something) and in your comments to this answer you allude to the desired behaviour being the other way around, so that's what this answer does.

This requires GNU sort for its -V option (version sort):

if cmp -s <(cut -d: -f2 infile) <(cut -d: -f2 infile | sort -V); then
echo 'FAILURE'
else
echo 'SUCCESS'
fi

This requires that the line with CurrV is always the first line. It extracts the parts after the colon with cut and compares the unsorted (first process substitution <(...)) to the version-sorted output (the second process substitution).

If they are the same, i.e., the version on the second line is greater than or equal to the one on the first line, the exit status of cmp is successful and we print FAILURE; if they aren't the same, this means that the sort inverted the order and the expected version is less than the current version, so we print SUCCESS.

The -s flag is to suppress output of cmp ("silent"); we're only interested in the exit status.


If you have 1.5.2 and 1.8.1 already in separate variables CurrV and ExpecV, you can do something similar as follows:

CurrV='1.5.2'
ExpecV='1.8.1'
printf -v versions '%s\n%s' "$CurrV" "$ExpecV"
if [[ $versions = "$(sort -V <<< "$versions")" ]]; then
echo 'FAILURE'
else
echo 'SUCCESS'
fi

This stores the two variables into versions, separated by a newline, then compares the unsorted with the sorted sequence.

Compare two version numbers (true if different minor version)

awk may help for this case. Here's a simple script to do that,

#!/bin/bash

old_version="$1"
new_version="$2"

awk -v u=${old_version} -v v=${new_version} '
BEGIN{
split(u,a,".");
split(v,b,".");
printf "old:%s new:%s => ",u,v;
for(i=1;i<=2;i++) if(b[i]!=a[i]){print "true";exit}
print "false"
}'

The script would only compare major and minor version number, return false if they're matched, else return true.

Comparing PHP version numbers using Bash?

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.

compare two comma separated strings

Another way to do this is through awk:

awk -F, -v master=$master_list '{ for (i=1;i<=NF;i++) { if (master ~ $i) { nomatch=0 } else { nomatch=1 } } } END { if ( nomatch==1 ) { print "absent" } else { print "present" } }' <<< $input

Set the field delimiter to , and then pass the master_list variable as master. Take each comma separated value in input and pattern match against the master. If there is a match set nomatch marked to 0 else set it to 1. At the end check the nomatch marker and print present or absent accordingly.



Related Topics



Leave a reply



Submit