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 withman 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
How to Use Variables in Bash Sed Command, Specific Example
Glibc Scanf Segmentation Faults When Called from a Function That Doesn't Align Rsp
Using Printf in Assembly Leads to Empty Output When Piping, But Works on the Terminal
How to Install Latest Version of Git on Centos 8.X/7.X/6.X
Find and Replace With Sed in Directory and Sub Directories
Find Multiple Files and Rename Them in Linux
How to Loop Over Directories in Linux
My Shell Script Stops After Exec
How to Use 'Cp' Command to Exclude a Specific Directory
How to Find Out What All Symbols Are Exported from a Shared Object
How to Symlink a File in Linux
How to Kill a Process Running on Particular Port in Linux
The 'Eval' Command in Bash and Its Typical Uses
Appending a Line to a File Only If It Does Not Already Exist
Avoid Gnome-Terminal Close After Script Execution
Run an Untrusted C Program in a Sandbox in Linux That Prevents It from Opening Files, Forking, etc.
Minimal Executable Size Now 10X Larger After Linking Than 2 Years Ago, For Tiny Programs