Skip to content

Instantly share code, notes, and snippets.

@akutz
Last active July 9, 2023 01:36
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save akutz/4bf84cce21dfb49dd55ca19014e2668f to your computer and use it in GitHub Desktop.
Save akutz/4bf84cce21dfb49dd55ca19014e2668f to your computer and use it in GitHub Desktop.
Comparing semantic version strings in POSIX shell

Overview

The attached script provides a function for comparing semantic versions in a POSIX shell script.

Result Description
-1 A<B
0 A=B
1 A>B

Usage

The following example compares 1.14 and v1.14.0 and determines the versions are equal:

$ semver.sh 1.14 v1.14.0
0

However, v1.14.0-alpha1 is less than 1.14.0.0:

$ semver.sh v1.14.0-alpha1 1.14.0.0
-1

Tests

It's possible for the script to validate itself by invoking it with TEST_SEMVER_COMP=1:

$ TEST_SEMVER_COMP=1 semver.sh 
a                                   b                                     exp   act
v1.0                                v1.0                                    0     0
v2.1.3                              v2.1.3.0                                0     0
v2.1.3                              2.1.3.0                                 0     0
v10.0                               v1.0                                    1     1
v1.0                                v10.0                                  -1    -1
v1.14                               v1.14.alpha1                            1     1
v1.14                               v1.14.0.alpha1                          1     1
v1.14.0                             v1.14.alpha1                            1     1
v1.14.alpha1                        v1.14                                  -1    -1
1.14.alpha1                         v1.14                                  -1    -1
v1.140.alpha1                       v1.14                                   1     1
v1.14                               v1.14.0-alpha.1.363                     1     1
v1.14.0-alpha.1.363+8bce3620b02b2a  1.14                                   -1    -1
#!/bin/sh
#
# usage: semver.sh A B
#
# Compares two semantic version strings and prints:
#
# -1 if A<B
# 0 if A=B
# 1 if A>B
#
# A non-zero exit code means there was an error comparing the
# provided values.
#
# The script may also be executed with TEST_SEMVER_COMP=1 to
# run several unit tests that validate the comparison logic.
# For example:
#
# $ TEST_SEMVER_COMP=1 semver.sh
# a b exp act
# v2.1.3 2.1.3.0 0 0
# v10.0 v1.0 1 1
# v1.0 v10.0 -1 -1
#
set -e
set -o pipefail
# A basic regex pattern for matching a semantic version string.
semverPatt='^\(v\{0,1\}\)\([[:digit:]]\{1,\}\)\{0,1\}\(.[[:digit:]]\{1,\}\)\{0,1\}\(.[[:digit:]]\{1,\}\)\{0,1\}\(.[[:digit:]]\{1,\}\)\{0,1\}\(.\{0,\}\)$'
# Returns a successful exit code IFF the provided string is a semver.
is_semver() {
echo "${1}" | grep -q "${semverPatt}"
}
# Extracts the MAJOR component of a semver.
get_major() {
echo "${1}" | sed -e 's/'"${semverPatt}"'/\2/g'
}
# Extracts the MINOR component of a semver.
get_minor() {
_v=$(echo "${1}" | sed -e 's/'"${semverPatt}"'/\3/g' | tr -d '.')
[ -n "${_v}" ] || _v=0; echo "${_v}"
}
# Extracts the PATCH component of a semver.
get_patch() {
_v=$(echo "${1}" | sed -e 's/'"${semverPatt}"'/\4/g' | tr -d '.')
[ -n "${_v}" ] || _v=0; echo "${_v}"
}
# Extracts the BUILD component of a semver.
get_build() {
_v=$(echo "${1}" | sed -e 's/'"${semverPatt}"'/\5/g' | tr -d '.')
[ -n "${_v}" ] || _v=0; echo "${_v}"
}
# Extracts the SUFFIX component of a semver.
get_suffix() {
echo "${1}" | sed -e 's/'"${semverPatt}"'/\6/g'
}
# Extracts the MAJOR.MINOR.PATCH.BUILD portion of a semver.
get_major_minor_patch_build() {
printf '%d.%d.%d.%d' \
"$(get_major "${1}")" \
"$(get_minor "${1}")" \
"$(get_patch "${1}")" \
"$(get_build "${1}")"
}
# Returns 0 if $1>$2
version_gt() {
test "$(printf '%s\n' "${@}" | sort -V | head -n 1)" != "${1}"
}
# Compares two semantic version strings:
# -1 if a<b
# 0 if a=b
# 1 if a>b
semver_comp() {
is_semver "${1}" || { echo "invalid semver: ${1}" 1>&2; return 1; }
is_semver "${2}" || { echo "invalid semver: ${2}" 1>&2; return 1; }
# Get the MAJOR.MINOR.PATCH.BUILD string for each version.
_a_mmpb="$(get_major_minor_patch_build "${1}")"
_b_mmpb="$(get_major_minor_patch_build "${2}")"
# Record whether or not the two MAJOR.MINOR.PATCH.BUILD are equal.
[ "${_a_mmpb}" = "${_b_mmpb}" ] && _a_eq_b=1
# Get the suffix components for each version.
_a_suffix="$(get_suffix "${1}")"
_b_suffix="$(get_suffix "${2}")"
# Reconstitute $1 and $2 as $_va and $_vb by filling in any
# components missing from the original semver values.
_va="${_a_mmpb}${_a_suffix}"
_vb="${_b_mmpb}${_b_suffix}"
# If the two reconstituted version strings are equal then the versions
# are equal.
if [ "${_va}" = "${_vb}" ]; then
_result=0
# If neither version have a suffix or if both versions have a suffix
# then the versions may be compared with sort -V.
elif { [ -z "${_a_suffix}" ] && [ -z "${_b_suffix}" ]; } || \
{ [ -n "${_a_suffix}" ] && [ -n "${_b_suffix}" ]; }; then
{ version_gt "${_va}" "${_vb}" && _result=1; } || _result=-1
# If $1 does not have a suffix and the two MAJOR.MINOR.PATCH.BUILD
# version strings are equal, then $1>$2.
elif [ -z "${_a_suffix}" ] && [ -n "${_a_eq_b}" ]; then
_result=1
# If $1 does have a suffix and the two MAJOR.MINOR.PATCH.BUILD
# version strings are equal, then $1<$2.
elif [ -n "${_a_suffix}" ] && [ -n "${_a_eq_b}" ]; then
_result=-1
# Otherwise compare the two versions using sort -V.
else
{ version_gt "${_va}" "${_vb}" && _result=1; } || _result=-1
fi
echo "${_result}"
}
[ -z "${TEST_SEMVER_COMP}" ] && { semver_comp "${@}"; exit "${?}"; }
printf '%-35s %-35s % 5s % 5s\n' 'a' 'b' 'exp' 'act'
test_semver_comp() {
result="$(semver_comp "${1}" "${2}")"
printf '%-35s %-35s % 5d % 5d\n' \
"${1}" "${2}" "${3}" "${result}"
}
test_semver_comp v1.0 v1.0 0
test_semver_comp v2.1.3 v2.1.3.0 0
test_semver_comp v2.1.3 2.1.3.0 0
test_semver_comp v10.0 v1.0 1
test_semver_comp v1.0 v10.0 -1
test_semver_comp v1.14 v1.14.alpha1 1
test_semver_comp v1.14 v1.14.0.alpha1 1
test_semver_comp v1.14.0 v1.14.alpha1 1
test_semver_comp v1.14.alpha1 v1.14 -1
test_semver_comp 1.14.alpha1 v1.14 -1
test_semver_comp v1.140.alpha1 v1.14 1
test_semver_comp v1.14 v1.14.0-alpha.1.363 1
test_semver_comp v1.14.0-alpha.1.363+8bce3620b02b2a 1.14 -1
exit 0
@chriswells0
Copy link

Hi, Andrew. I used this script in a small personal project that monitors containers in a K8s cluster to automatically upgrade their images. I'd like to release the project on GitHub. Obviously, you shared the code; however, there is no license on a gist. Is this script part of any licensed project? If not, would you be OK with me using it? If so, please let me know if you have a license preference (it appears you prefer Apache 2, and I lean toward that or the BSD 3-clause) as well as how you'd prefer the copyright to appear. Also, if you'd like, I could create the repo first and let you add the script as a PR. Thanks!

@akutz
Copy link
Author

akutz commented Feb 22, 2023

Hi @chriswells0,

Feel free to reuse it however you wish. If you want to link back to this gist in a comment at the top for credit, that's fine too. I am fine with either license you pick between the two you listed. As for how the license should appear, you can refer to https://github.com/akutz/simple-k8s-test-env/blob/master/sk8.sh for an example of how I usually add license info to the top of scripts.

You could also just add us each as an author to the top of the file, ex.

#!/bin/sh

# authors:
#   Andrew Kutz <sakutz@gmail.com>
#   Chris Wells <YourInfo>

#
# usage: semver.sh A B

Ultimately it's up to you. Still, thank you for reaching out and asking. Quite a nice thing to do 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment