citadel-core/scripts/citadel-os/semver
2022-01-21 21:37:48 +01:00

526 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: Copyright (c) 2014 Józef Sokołowski
# https://github.com/qzb/sh-semver
# Commit: 2ac2437
_num_part='([0-9]|[1-9][0-9]*)'
_lab_part='([0-9]|[1-9][0-9]*|[0-9]*[a-zA-Z-][a-zA-Z0-9-]*)'
_met_part='([0-9A-Za-z-]+)'
RE_NUM="$_num_part(\.$_num_part)*"
RE_LAB="$_lab_part(\.$_lab_part)*"
RE_MET="$_met_part(\.$_met_part)*"
RE_VER="[ \t]*$RE_NUM(-$RE_LAB)?(\+$RE_MET)?"
BRE_DIGIT='[0-9]\{1,\}'
BRE_ALNUM='[0-9a-zA-Z-]\{1,\}'
BRE_IDENT="$BRE_ALNUM\(\.$BRE_ALNUM\)*"
BRE_MAJOR="$BRE_DIGIT"
BRE_MINOR="\(\.$BRE_DIGIT\)\{0,1\}"
BRE_PATCH="\(\.$BRE_DIGIT\)\{0,1\}"
BRE_PRERE="\(-$BRE_IDENT\)\{0,1\}"
BRE_BUILD="\(+$BRE_IDENT\)\{0,1\}"
BRE_VERSION="${BRE_MAJOR}${BRE_MINOR}${BRE_PATCH}${BRE_PRERE}${BRE_BUILD}"
filter()
{
local text="$1"
local regex="$2"
shift 2
echo "$text" | grep -E "$@" "$regex"
}
# Gets number part from normalized version
get_number()
{
echo "${1%%-*}"
}
# Gets prerelase part from normalized version
get_prerelease()
{
local pre_and_meta=${1%+*}
local pre=${pre_and_meta#*-}
if [ "$pre" = "$1" ]; then
echo
else
echo "$pre"
fi
}
# Gets major number from normalized version
get_major()
{
echo "${1%%.*}"
}
# Gets minor number from normalized version
get_minor()
{
local minor_major_bug=${1%%-*}
local minor_major=${minor_major_bug%.*}
local minor=${minor_major#*.}
if [ "$minor" = "$minor_major" ]; then
echo
else
echo "$minor"
fi
}
get_bugfix()
{
local minor_major_bug=${1%%-*}
local bugfix=${minor_major_bug##*.*.}
if [ "$bugfix" = "$minor_major_bug" ]; then
echo
else
echo "$bugfix"
fi
}
strip_metadata()
{
echo "${1%+*}"
}
semver_eq()
{
local ver1 ver2 part1 part2
ver1=$(get_number "$1")
ver2=$(get_number "$2")
local count=1
while true; do
part1=$(echo "$ver1"'.' | cut -d '.' -f $count)
part2=$(echo "$ver2"'.' | cut -d '.' -f $count)
if [ -z "$part1" ] || [ -z "$part2" ]; then
break
fi
if [ "$part1" != "$part2" ]; then
return 1
fi
local count=$(( count + 1 ))
done
if [ "$(get_prerelease "$1")" = "$(get_prerelease "$2")" ]; then
return 0
else
return 1
fi
}
semver_lt()
{
local number_a number_b prerelease_a prerelease_b
number_a=$(get_number "$1")
number_b=$(get_number "$2")
prerelease_a=$(get_prerelease "$1")
prerelease_b=$(get_prerelease "$2")
local head_a=''
local head_b=''
local rest_a=$number_a.
local rest_b=$number_b.
while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do
head_a=${rest_a%%.*}
head_b=${rest_b%%.*}
rest_a=${rest_a#*.}
rest_b=${rest_b#*.}
if [ -z "$head_a" ] || [ -z "$head_b" ]; then
return 1
fi
if [ "$head_a" -eq "$head_b" ]; then
continue
fi
if [ "$head_a" -lt "$head_b" ]; then
return 0
else
return 1
fi
done
if [ -n "$prerelease_a" ] && [ -z "$prerelease_b" ]; then
return 0
elif [ -z "$prerelease_a" ] && [ -n "$prerelease_b" ]; then
return 1
fi
local head_a=''
local head_b=''
local rest_a=$prerelease_a.
local rest_b=$prerelease_b.
while [ -n "$rest_a" ] || [ -n "$rest_b" ]; do
head_a=${rest_a%%.*}
head_b=${rest_b%%.*}
rest_a=${rest_a#*.}
rest_b=${rest_b#*.}
if [ -z "$head_a" ] && [ -n "$head_b" ]; then
return 0
elif [ -n "$head_a" ] && [ -z "$head_b" ]; then
return 1
fi
if [ "$head_a" = "$head_b" ]; then
continue
fi
# If both are numbers then compare numerically
if [ "$head_a" = "${head_a%[!0-9]*}" ] && [ "$head_b" = "${head_b%[!0-9]*}" ]; then
[ "$head_a" -lt "$head_b" ] && return 0 || return 1
# If only a is a number then return true (number has lower precedence than strings)
elif [ "$head_a" = "${head_a%[!0-9]*}" ]; then
return 0
# If only b is a number then return false
elif [ "$head_b" = "${head_b%[!0-9]*}" ]; then
return 1
# Finally if of identifiers is a number compare them lexically
else
test "$head_a" \< "$head_b" && return 0 || return 1
fi
done
return 1
}
semver_gt()
{
if semver_lt "$1" "$2" || semver_eq "$1" "$2"; then
return 1
else
return 0
fi
}
semver_le()
{
semver_gt "$1" "$2" && return 1 || return 0
}
semver_ge()
{
semver_lt "$1" "$2" && return 1 || return 0
}
semver_sort()
{
if [ $# -le 1 ]; then
echo "$1"
return
fi
local pivot=$1
local args_a=()
local args_b=()
shift 1
for ver in "$@"; do
if semver_le "$ver" "$pivot"; then
args_a=( "${args_a[@]}" "$ver" )
else
args_b=( "$ver" "${args_b[@]}" )
fi
done
args_a=( $(semver_sort "${args_a[@]}") )
args_b=( $(semver_sort "${args_b[@]}") )
echo "${args_a[@]}" "$pivot" "${args_b[@]}"
}
regex_match()
{
local string="$1 "
local regexp="$2"
local match
match="$(eval "echo '$string' | grep -E -o '^[ \t]*($regexp)[ \t]+'")";
for i in $(seq 0 9); do
unset "MATCHED_VER_$i"
unset "MATCHED_NUM_$i"
done
unset REST
if [ -z "$match" ]; then
return 1
fi
local match_len=${#match}
REST="${string:$match_len}"
local part
local i=1
for part in $string; do
local ver num
ver="$(eval "echo '$part' | grep -E -o '$RE_VER' | head -n 1 | sed 's/ \t//g'")";
num=$(get_number "$ver")
if [ -n "$ver" ]; then
eval "MATCHED_VER_$i='$ver'"
eval "MATCHED_NUM_$i='$num'"
i=$(( i + 1 ))
fi
done
return 0
}
# Normalizes rules string
#
# * replaces chains of whitespaces with single spaces
# * replaces whitespaces around hyphen operator with "_"
# * removes wildcards from version numbers (1.2.* -> 1.2)
# * replaces "x" with "*"
# * removes whitespace between operators and version numbers
# * removes leading "v" from version numbers
# * removes leading and trailing spaces
normalize_rules()
{
echo " $1" \
| sed 's/\\t/ /g' \
| sed 's/ / /g' \
| sed 's/ \{2,\}/ /g' \
| sed 's/ - /_-_/g' \
| sed 's/\([~^<>=]\) /\1/g' \
| sed 's/\([ _~^<>=]\)v/\1/g' \
| sed 's/\.[xX*]//g' \
| sed 's/[xX]/*/g' \
| sed 's/^ //g' \
| sed 's/ $//g'
}
# Reads rule from provided string
resolve_rule()
{
local rule operator operands
rule="$1"
operator="$( echo "$rule" | sed "s/$BRE_VERSION/#/g" )"
operands=( $( echo "$rule" | grep -o "$BRE_VERSION") )
case "$operator" in
'*') echo "all" ;;
'#') echo "eq ${operands[0]}" ;;
'=#') echo "eq ${operands[0]}" ;;
'<#') echo "lt ${operands[0]}" ;;
'>#') echo "gt ${operands[0]}" ;;
'<=#') echo "le ${operands[0]}" ;;
'>=#') echo "ge ${operands[0]}" ;;
'#_-_#') echo "ge ${operands[0]}"
echo "le ${operands[1]}" ;;
'~#') echo "tilde ${operands[0]}" ;;
'^#') echo "caret ${operands[0]}" ;;
*) return 1
esac
}
resolve_rules()
{
local rules
rules="$(normalize_rules "$1")"
IFS=' ' read -ra rules <<< "${rules:-all}"
for rule in "${rules[@]}"; do
resolve_rule "$rule"
done
}
rule_eq()
{
local rule_ver="$1"
local tested_ver="$2"
semver_eq "$tested_ver" "$rule_ver" && return 0 || return 1;
}
rule_le()
{
local rule_ver="$1"
local tested_ver="$2"
semver_le "$tested_ver" "$rule_ver" && return 0 || return 1;
}
rule_lt()
{
local rule_ver="$1"
local tested_ver="$2"
semver_lt "$tested_ver" "$rule_ver" && return 0 || return 1;
}
rule_ge()
{
local rule_ver="$1"
local tested_ver="$2"
semver_ge "$tested_ver" "$rule_ver" && return 0 || return 1;
}
rule_gt()
{
local rule_ver="$1"
local tested_ver="$2"
semver_gt "$tested_ver" "$rule_ver" && return 0 || return 1;
}
rule_tilde()
{
local rule_ver="$1"
local tested_ver="$2"
if rule_ge "$rule_ver" "$tested_ver"; then
local rule_major rule_minor
rule_major=$(get_major "$rule_ver")
rule_minor=$(get_minor "$rule_ver")
if [ -n "$rule_minor" ] && rule_eq "$rule_major.$rule_minor" "$(get_number "$tested_ver")"; then
return 0
fi
if [ -z "$rule_minor" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then
return 0
fi
fi
return 1
}
rule_caret()
{
local rule_ver="$1"
local tested_ver="$2"
if rule_ge "$rule_ver" "$tested_ver"; then
local rule_major
rule_major="$(get_major "$rule_ver")"
if [ "$rule_major" != "0" ] && rule_eq "$rule_major" "$(get_number "$tested_ver")"; then
return 0
fi
if [ "$rule_major" = "0" ] && rule_eq "$rule_ver" "$(get_number "$tested_ver")"; then
return 0
fi
fi
return 1
}
rule_all()
{
return 0
}
apply_rules()
{
local rules_string="$1"
shift
local versions=( "$@" )
# Loop over sets of rules (sets of rules are separated with ||)
for ver in "${versions[@]}"; do
rules_tail="$rules_string";
while [ -n "$rules_tail" ]; do
head="${rules_tail%%||*}"
if [ "$head" = "$rules_tail" ]; then
rules_string=""
else
rules_tail="${rules_tail#*||}"
fi
#if [ -z "$head" ] || [ -n "$(echo "$head" | grep -E -x '[ \t]*')" ]; then
#group=$(( $group + 1 ))
#continue
#fi
rules="$(resolve_rules "$head")"
# If specified rule cannot be recognised - end with error
if [ $? -eq 1 ]; then
exit 1
fi
if ! echo "$ver" | grep -q -E -x "[v=]?[ \t]*$RE_VER"; then
continue
fi
ver=$(echo "$ver" | grep -E -x "$RE_VER")
success=true
allow_prerel=false
if $FORCE_ALLOW_PREREL; then
allow_prerel=true
fi
while read -r rule; do
comparator="${rule%% *}"
operand="${rule#* }"
if [ -n "$(get_prerelease "$operand")" ] && semver_eq "$(get_number "$operand")" "$(get_number "$ver")" || [ "$rule" = "all" ]; then
allow_prerel=true
fi
"rule_$comparator" "$operand" "$ver"
if [ $? -eq 1 ]; then
success=false
break
fi
done <<< "$rules"
if $success; then
if [ -z "$(get_prerelease "$ver")" ] || $allow_prerel; then
echo "$ver"
break;
fi
fi
done
group=$(( group + 1 ))
done
}
FORCE_ALLOW_PREREL=false
USAGE="Usage: $0 [-r <rule>] [<version>... ]
Omitting <version>s reads them from STDIN.
Omitting -r <rule> simply sorts the versions according to semver ordering."
while getopts ar:h o; do
case "$o" in
a) FORCE_ALLOW_PREREL=true ;;
r) RULES_STRING="$OPTARG||";;
h) echo "$USAGE" && exit ;;
?) echo "$USAGE" && exit 1;;
esac
done
shift $(( OPTIND-1 ))
VERSIONS=( ${@:-$(cat -)} )
# Sort versions
VERSIONS=( $(semver_sort "${VERSIONS[@]}") )
if [ -z "$RULES_STRING" ]; then
printf '%s\n' "${VERSIONS[@]}"
else
apply_rules "$RULES_STRING" "${VERSIONS[@]}"
fi