#!/usr/bin/env bash # SPDX-License-Identifier: AGPL-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 ] [... ] Omitting s reads them from STDIN. Omitting -r 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