Comparing two versions [duplicate] - bash

This question already has answers here:
How to compare two strings in dot separated version format in Bash?
(38 answers)
Closed 4 months ago.
I am have the toughest time with this and was wondering if anyone can help. I'm trying to compare two versions and output something if a version is old. Here is an example of what I have.
monterey="17612.4.9.1.8"
version=$(defaults read /Applications/Safari.app/Contents/Info.plist CFBundleVersion)
if [ "$version" -ge "$monterey" ] ; then
echo "Up to date"
else
echo "Needs update"
fi
exit 0
What I'd like for it to do is compare the Safari "version" version to the "monterey" version. If Safari is greater than or equal to "Monterey" then echo "Up to date".
But every time I try to do this I get "integer expression expected" or if I try >= I get "unary operator expected".
How should this be written?

17612.4.9.1.8 cannot be considered as an integer. Not even as a number: too many dots. If you want to compare dot-separated versions you must do this one field at a time, starting from the major version number.
One option is to store the fields in an array:
$ m=(${monterey//./ })
$ echo ${m[0]}
17612
$ echo ${#m[#]}
5
m=(${monterey//./ }) replaces all dots in $monterey by spaces and stores the result in array m, one space-separated word per cell. ${#m[#]} expands as the size of the array. So, something like the following should do what you want:
m=(${monterey//./ })
v=(${version//./ })
(( n = ${#v[#]} < ${#m[#]} ? ${#m[#]} : ${#v[#]} ))
for (( i=0; i < n; i++ )); do
if (( ${v[i]:-0} < ${m[i]:-0} )); then
echo "Needs update"
break
elif (( ${v[i]:-0} > ${m[i]:-0} )); then
echo "Up to date"
break
fi
done
exit 0
(( n = ${#v[#]} < ${#m[#]} ? ${#m[#]} : ${#v[#]} )) stores the largest array size in variable n. ${v[i]:-0} expands as v[i] if it is set and not the empty string, else as 0.
But if you can use sort, instead of plain bash, you can also use:
l=$(printf '%s\n%s\n' "$monterey" "$version" | sort -nt. | tail -n1)
if [[ $version = $l ]]; then
echo "Up to date"
else
echo "Needs update"
fi
exit 0
The first command sorts the two versions numerically (-n), using . as separator (-t.), keeps only the last (tail -n1), that is, the largest, and stores it in variable l. Note that this could not work as you would like if you can have trailing 0 fields: 1.2.0.0 will be considered as larger than 1.2.0 or 1.2.
As mentioned by #markp-fuso, if your sort utility supports it (it is a non-POSIX feature found, for instance, in the GNU coreutils sort), you can also use its -V option that does exactly what you want:
l=$(printf '%s\n%s\n' "$monterey" "$version" | sort -V | tail -n1)

-ge is used for numerical comparison operations.
This is to determine whether the strings are equal, you should use [ "$version" = "$monterey" ]

Related

Formatting Output From Shell Script [duplicate]

This question already has answers here:
Echo tab characters in bash script
(10 answers)
Closed 5 years ago.
I am working on a shell script that takes stdin or file as input and prints the averages and medians for rows or columns depending on the arguments.
When calculating the averages for the columns, the output needs to print out the following (tabbed):
My output currently looks like this (no spaces or tabs):
Averages:
92480654263
Medians:
6368974
Is there a way to echo out the averages and medians with tabs so each average and median set align left correctly? Here is a sample of how I am printing out the averages:
echo "Averages:"
while read i
do
sum=0
count=0
mean=0
#Cycle through the numbers in the rows
for num in $i
do
#Perform calculations necessary to determine the average and median
sum=$(($sum + $num))
count=`expr $count + 1`
mean=`expr $sum / $count`
done
echo -n "$mean"
done < $1
man echo:
-e enable interpretation of backslash escapes
If -e is in effect, the following sequences are recognized:
\t horizontal tab
I'd try echo -n -e "$mean\t", didn't test it though.
You should use printf. For instance, this will print a value followed by a tab
printf "%s\t" "$mean"
You can actually print several values separated by tabs if you want by adding arguments :
printf "%s\t" "$mean" "$count"
You can use an array expansion to print several values separated by tabs :
printf "%s\t" "${my_array[#]}"
Among advantages of printf over echo is the availability of flexible formatting strings, and the fact that implementations of printf vary less than those of echo among shells and operating systems.
You could try using column command but it does take additional steps:
echo "Averages:"
while read line
do
sum=0
count=0
mean=0
#Cycle through the numbers in the rows
for num in $line
do
#Perform calculations necessary to determine the average and median
(( sum += num ))
(( count++ ))
(( mean = sum / count ))
done
(( mean == 0 )) && out=$mean || out="$out|$mean"
done < $1
echo "$out" | column -s'|' -t
Above is untested as I do not have the original file, but you should get the idea. I would add that the division will also provide truncated values so not exactly accurate.

(standard_in) 1: syntax error, comparing floating point no using bc in bash [duplicate]

I have this script that should make sure that the users current PHP version is between a certain range, though it SHOULD work, there is a bug somewhere that makes it think that the version is out of range, could someone take a look and tell me what I can do to fix it?
function version { echo "$#" | gawk -F. '{ printf("%d.%d.%d\n", $1,$2,$3); }'; }
phpver=`php -v |grep -Eow '^PHP [^ ]+' |gawk '{ print $2 }'`
if [ $(version $phpver) > $(version 5.2.13) ] || [ $(version $phpver) < $(version 5.2.13) ]; then
echo "PHP Version $phpver must be between 5.2.13 - 5.3.15"
exit
fi
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.
There is possibility for deb-distributions:
dpkg --compare-versions <version> <relation> <version>
For example:
dpkg --compare-versions "0.0.4" "gt" "0.0.3"
if [ $? -eq "0" ]; then echo "YES"; else echo "NO"; fi
It is doing a lexical comparison. Use one of these:
if [ $(version $phpver) -gt $(version 5.2.13) ] || [ $(version $phpver) -lt $(version 5.2.13) ]; then
if [[ $(version $phpver) > $(version 5.2.13) ]] || [[ $(version $phpver) < $(version 5.2.13) ]]; then
if (( $(version $phpver) > $(version 5.2.13) )) || (( $(version $phpver) < $(version 5.2.13) )); then
Or do it all in awk or some other tool. It is screaming for some optimisation. It also seems you're not producing numbers either, so you have a pretty odd design. Usually the version substrings are multiplied by 1000 and then all summed up to get a single comparable scalar.
Here's another solution that:
does not run any external command apart from tr
has no restriction on number of parts in version string
can compare version strings with different number of parts
Note that it's Bash code using array variables.
compare_versions()
{
local v1=( $(echo "$1" | tr '.' ' ') )
local v2=( $(echo "$2" | tr '.' ' ') )
local len="$(max "${#v1[*]}" "${#v2[*]}")"
for ((i=0; i<len; i++))
do
[ "${v1[i]:-0}" -gt "${v2[i]:-0}" ] && return 1
[ "${v1[i]:-0}" -lt "${v2[i]:-0}" ] && return 2
done
return 0
}
The function returns:
0 if versions are equal (btw: 1.2 == 1.2.0)
1 if the 1st version is bigger / newer
2 if the 2nd version is bigger / newer
However #1 -- it requires one additional function (but function min is quite usable to have anyway):
min()
{
local m="$1"
for n in "$#"
do
[ "$n" -lt "$m" ] && m="$n"
done
echo "$m"
}
However #2 -- it cannot compare version strings with alpha-numeric parts (though that would not be difficult to add, actually).
A much safer option for testing the PHP CLI version is to use PHP's own version_compare function.
#!/bin/bash
MIN_VERSION="7.0.0"
MAX_VERSION="7.1.4"
PHP_VERSION=`php -r 'echo PHP_VERSION;'`
function version_compare() {
COMPARE_OP=$1;
TEST_VERSION=$2;
RESULT=$(php -r 'echo version_compare(PHP_VERSION, "'${TEST_VERSION}'", "'${COMPARE_OP}'") ? "TRUE" : "";')
test -n "${RESULT}";
}
if ( version_compare "<" "${MIN_VERSION}" || version_compare ">" "${MAX_VERSION}" ); then
echo "PHP Version ${PHP_VERSION} must be between ${MIN_VERSION} - ${MAX_VERSION}";
exit 1;
fi
echo "PHP Version ${PHP_VERSION} is good!";
The following solution should more accurately addresses your exact need. It can be used to test whether the CURRENT version string falls between MIN and MAX. I am assuming that MIN and MAX are acceptable version numbers (i.e. MIN <= CURRENT <= MAX rather than MIN < CURRENT < MAX).
# Usage: version MIN CURRENT MAX
version(){
local h t v
[[ $2 = "$1" || $2 = "$3" ]] && return 0
v=$(printf '%s\n' "$#" | sort -V)
h=$(head -n1 <<<"$v")
t=$(tail -n1 <<<"$v")
[[ $2 != "$h" && $2 != "$t" ]]
}
For example...
if ! version 5.2.13 "$phpver" 5.3.15; then
echo "PHP Version $phpver must be between 5.2.13 - 5.3.15"
exit
fi
If you're on Bash 3 with an older version of sort (lookin at you macOS...), then I created the following helper script you can source in (can also be ran as a command):
https://github.com/unicorn-fail/version_compare
I wrote this inelegant function a while back for a similar problem. vers_limit will return 0 if arg1 is less than or equal to arg2, non-0 otherwise:
vers_limit()
{
VERNEW=$1
VERLMT=$2
CHKNEW=$VERNEW
CHKLMT=$VERLMT
PASSED=
while :
do
PARTNEW=${CHKNEW%%.*}
PARTLMT=${CHKLMT%%.*}
if [[ $PARTNEW -lt $PARTLMT ]]
then
PASSED=GOOD
break
elif [[ $PARTNEW -gt $PARTLMT ]]
then
PASSED=
break
else
NXTNEW=${CHKNEW#*.}
if [[ $NXTNEW == $CHKNEW ]]
then
if [[ $NXTNEW == $CHKLMT ]]
then
PASSED=GOOD
break
else
NXTNEW=0
fi
fi
NXTLMT=${CHKLMT#*.}
if [[ $NXTLMT == $CHKLMT ]]
then
NXTLMT=0
fi
fi
CHKNEW=$NXTNEW
CHKLMT=$NXTLMT
done
test "$PASSED"
}
This is not as compact as some of the other solutions here, nor does it provide 3-way status (i.e., less, equal, greater), though I believe one can order the arguments, interpret the pass/fail status, and/or call twice to accomplish any desired result. That said, vers_limit does have certain virtues:
No calls to external utilities such as sort, awk, gawk, tr, etc.
Handles numeric versions of arbitrary size (up to the shell's
limit for integer calculations)
Handles an arbitrary number of "dotted" parts.
v_min="5.2.15"
v_max="5.3.15"
v_php="$(php -v | head -1 | awk '{print $2}')"
[ ! "$v_php" = "$(echo -e "$v_php\n$v_min\n$v_max" | sort -V | head -2 | tail -1)" ] && {
echo "PHP Version $v_php must be between $v_min - $v_max"
exit
}
This puts v_min, v_max and v_php in version order and tests if v_php is not in the middle.
PURE BASH
I found my way to this page because I had the same problem. The other answers did not satisfy me, so I wrote this function.
So long as the 2 versions have the same number of periods in them this will compare the versions correctly.
It does a c style for loop, setting $i incrementally from 0 to # of numbers in the version string.
for each #:
if new - old is neg we know the first version is newer.
If new - old is pos we know the first version is older.
If new - old is 0 then it is the same and we need to continue checking.
We run false after to set exit status of the function for the case where $1 == $2 the versions are totally identical.
newver=1.10.1
installedver=1.9.25
#installedver=1.11.25
#installedver=1.10.1
checkupdate(){
# $1 = new version
# $2 = installed version
IFS='.' read -r -a nver <<< "$1"
IFS='.' read -r -a iver <<< "$2"
for ((i = 0 ; i < "${#nver[#]}" ; i++)) ;do
case "$((${nver[i]}-${iver[i]}))" in
-*) return 1 ;;
0) ;;
*) return 0 ;;
esac
false
done
}
checkupdate "$newver" "$installedver" && echo yes || echo no
Another method for SH
After I tried to implement my above function on Android I realized that I would not always have bash, so the above function did not work for me. Here is the version I wrote using awk to get around needing bash:
checkupdate(){
# $1 = new version
# $2 = installed version
i=1
#we start at 1 and go until number of . so we can use our counter as awk position
places=$(awk -F. '{print NF+1}' <<< "$1")
while (( "$i" < "$places" )) ;do
npos=$(awk -v pos=$i -F. '{print $pos}' <<< "$1")
ipos=$(awk -v pos=$i -F. '{print $pos}' <<< "$2")
case "$(( $npos - $ipos ))" in
-*) return 1 ;;
0) ;;
*) return 0 ;;
esac
i=$((i+1))
false
done
}

Compare Decimals in Bash while Loop

In the below code, ShellCheck throws an error in the while clause.
count=10.0
while [ $count -le 20.0 ]
do
echo "Hello"
count=$(bc<<< "scale=4; (count+0.1)")
done
ShellCheck says:
Decimals not supported, either use integers or bc
I am not quite sure how to use bc in a while loop.
while [ $(bc <<< "scale=4; (count -le 20.0)" ]
How do I compare decimal numbers in a while clause? Any advice?
Bash doesn't support floating point arithmetic.
You can either use bc:
count="10.0"
limit="12.0"
increment="0.1"
while [ "$(bc <<< "$count < $limit")" == "1" ]; do
echo "Hello"
count=$(bc <<< "$count+$increment")
done
or awk:
while awk 'BEGIN { if ('$count'>='$limit') {exit 1}}'; do
echo "Hello"
count=$(bc <<< "$count+$increment")
done
I just wonder: why not (directly) count from 10.0 to 12.0 ?
for i in $(seq 10.0 0.1 12.0); do
echo "Hello"
done
Bash doesn't support floating pointing arithmetic. You can use bc for that comparison too:
count=10.0
while : ;
do
out=$(bc -l<<< "$count<20.0")
[[ $out == 0 ]] && { echo "Reached limit" ; exit 0; }
echo "Hello"
count=$(bc<<< "scale=4; ($count+0.1)")
done
Note that I added the missing $ to count inside the loop where you update count.
While bash doesn't handle floating point numbers, the seq utility does. [Note 1]
The basic syntax is seq FIRST INCREMENT LAST, so in your case you could use
for count in "$(seq 10.0 0.1 20.0)"; do
# something with $count
done
If you provide two arguments, they are assumed to be FIRST and LAST, with INCREMENT being 1. If you provide only one argument, it is assumed to be LAST, with both FIRST and INCREMENT being 1. As in your example, the sequence is inclusive so both FIRST and LAST will be produced provided that INCREMENT evenly divides FIRST−LAST.
You can also include an explicit format:
$ seq -f "%06.3f" 1 .5 2
01.000
01.500
02.000
One downside of this technique is that it precomputes the entire collection of values. If the loop will execute hundreds of thousands of times, that might use up a lot of memory, in which case you could use a pipe or process substitution instead:
while read count; do
# something with count
done < <(seq 10.0 0.000001 20.0)
Notes
seq is not Posix but it is almost always present; it's part of GNU coreutils and a similar utility, available in Mac OS X) has been in NetBSD since 3.0 and FreeBSD since 9.0.

Bash - Stripping and adding leading zeros to numbers before concatenating into string ordered strings

I need to automate a backup solution which stores files in folders such as YYYYMMDD.nn.
Every day few files would be backed up like this so the resulting folder names could be 20141002.01, 20141002.2 ... 20141002.10. My current script works for YYYYMMDD.n but when n is more than 9 sorting and picking up the last folder doesn't work because 20141002.10 is above 20141002.9 hens switching to YYYYMMDD.nn format and the approach of separating the nn, stripping leading zeros, then incrementing, and adding leading zeros if needed.
I have a function which checks the last folder for today's date and creates the next one.
createNextProcessedFolder() {
local LastFolderName=`ls -1 ${ProcessedListsDir} | grep ${CurrentDate} | tail -n 1`
n=`echo ${LastFolderName} | sed -r 's/^.{9}//'`
n="$((10#$n))"
nextFolderName=${CurrentDate}.$((if[[ $(( ${n}+1 )) < 10 ]];then n="0$((${n}+1))";else n="$(( ${n}+1 ))"; fi))
mkdir ${ProcessedListsDir}/${nextFolderName}
if [[ -d ${ProcessedListsDir}/${nextFolderName} ]]
then
echo "New folder ${nextFolderName} was created"
else
echo "Error: ${nextFolderName} was not created"
fi
Location="${ProcessedListsDir}/${nextFolderName}"
}
So when I try to run this I get an error like:
line 21: if[[ 1 < 10 ]];then n="01";else n="1"; fi: syntax error: invalid arithmetic operator (error token is ";then n="01";else n="1"; fi")
Line 21 is:
nextFolderName=${CurrentDate}.$((if[[ $(( ${n}+1 )) < 10 ]];then n="0$((${n}+1))";else n="$(( ${n}+1 ))"; fi))
I'm sure there will be more errors after this one but I would really appreciate if somebody helped me with this.
You cannot use $((...)) for command substitution as it needs to be $(...)
You need spaces before and after [[ and ]]. You can also use ((...)) in BASH:
Try this:
(( (n+1) < 10 )) && n="0$((n++))" || ((n++))
nextFolderName="${CurrentDate}.${n}"
For completeness, another solution is:
n=$( printf "%02d" $n )
The 02 before the d means prepend with 0s up to 2 digits. Or:
nextFolderName="${CurrentDate}."$( printf "%02d" "$n" )
So my problem was with incrementing a number witch was extracted from a string with a leading zero and then returning the incremented number with a leading zero if smaller than 10. The solution I ended up using can be represented with the below script.
I guess it can't be shorter than that
n=$1
(( ((n++)) < 10 )) && n="0$n"
echo $n
Something I didn't expect is that I don't have to strip leading zeros from n using this, n++ does it while incrementing :-)
Thanks again anubhava for pointing me in the right direction.

Match first few letters of a file name : Shell script

I am trying to match first few letters of a file.
for entry in `ls`; do
echo $entry
done
With the above code I get the name of all the files.
I have a few files with similar name at the start:
Beaglebone-v1
Beaglebone-v3
Beaglebone-v2
How can I compare $entry with Beaglebone* and then extract the latest version file name?
If you want to loop over all Beaglebone-* files:
for entry in Beaglebone-* ; do
echo $entry
done
if you just need the file with the latest version, you can depend on the fact that ls sorts your names alphabetically, so you could just do:
LATEST_FILE_NAME=$(ls Beaglebone-* | tail -n 1)
which will just take the last one alphabetically.
To deal with larger numbers, you could use numeric comparison like this:
stem="Beaglebone-v"
for file in $stem*; do
ver=${file#"$stem"} # cut away stem to get version number
(( ver > max )) && max=$ver # conditionally assign `ver` to `max`
done
echo "$stem$max"
Testing it out:
bash-4.3$ ls Beaglebone-v*
Beaglebone-v1 Beaglebone-v10 Beaglebone-v2 Beaglebone-v3
bash-4.3$ stem="Beaglebone-v" &&
for file in $stem*
do
ver=${file#"$stem"}
(( ver > max )) && max=$ver
done; echo "$stem$max"
Beaglebone-v10
You can store the filenames matching the pattern in an array and then pick the last element of the array.
shopt -s nullglob
arr=( Beaglebone-* )
if (( ${#arr[#]} > 0 ))
then
latest="${arr[ (( ${#arr[#]} - 1 )) ]}"
echo "$latest"
fi
You need to enable nullglob so that if there are no files matching the pattern, you will get an empty array rather than the pattern itself.
If version numbers can go beyond single digits,
function version_numbers {
typeset w; for w in $1-v*; do echo ${w#$1-v}; done
}
version_numbers "Beaglebone" | sort -n | tail -1
Or, adding function max:
# read a stream of numbers, from stdin (one per line)
# and return the largest value
function max
{
typeset _max n
read _max || return
while read n; do
((_max < n)) && _max=$n
done
echo $_max
}
We can now do the whole thing without external commands:
version_numbers Beaglebone | max
Note that max will fail horribly if any one line fails the numerical comparison.

Resources