|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +set -euo pipefail |
| 4 | + |
| 5 | +# Validate a semver string, exit if invalid. |
| 6 | +# Arguments: |
| 7 | +# $1: semver string to validate |
| 8 | +validate_semver() { |
| 9 | + |
| 10 | + # Regex for a semver digit |
| 11 | + D='0|[1-9][0-9]*' |
| 12 | + # Regex for a semver pre-release word |
| 13 | + PW='[0-9]*[a-zA-Z-][0-9a-zA-Z-]*' |
| 14 | + # Regex for a semver build-metadata word |
| 15 | + MW='[0-9a-zA-Z-]+' |
| 16 | + |
| 17 | + # Regex pattern to match a semver string with numeric version identifiers, pre-release identifiers, and build metadata |
| 18 | + semver_pattern="^($D)\.($D)\.($D)(-(($D|$PW)(\.($D|$PW))*))?(\+($MW(\.$MW)*))?$" |
| 19 | + |
| 20 | + if ! [[ $1 =~ $semver_pattern ]]; then |
| 21 | + echo "$0: Invalid semver string: $1" >&2 |
| 22 | + echo "$0: Expected format 'x.y.z' or 'x.y.z-pre-release'." |
| 23 | + exit 1 |
| 24 | + fi |
| 25 | +} |
| 26 | + |
| 27 | +# Function to compare two component strings. |
| 28 | +# Arguments: |
| 29 | +# $1: First component string |
| 30 | +# $2: Second component string |
| 31 | +# Output: |
| 32 | +# -1 if the first component has lower precedence |
| 33 | +# 1 if the first component has higher precedence |
| 34 | +compare_components() { |
| 35 | + if (($1 > $2)); then |
| 36 | + echo 1 && exit 0 |
| 37 | + elif (($1 < $2)); then |
| 38 | + echo -1 && exit 0 |
| 39 | + fi |
| 40 | +} |
| 41 | + |
| 42 | +# Function to compare pre-release identifiers. |
| 43 | +# Arguments: |
| 44 | +# $1: First pre-release identifier |
| 45 | +# $2: Second pre-release identifier |
| 46 | +# Output: |
| 47 | +# -1 if the first pre-release identifier has lower precedence |
| 48 | +# 0 if both identifiers are equal |
| 49 | +# 1 if the first pre-release identifier has higher precedence |
| 50 | +compare_prerelease() { |
| 51 | + # If both pre-release identifiers are empty, they are considered equal |
| 52 | + if [ -z "$1" ] && [ -z "$2" ]; then |
| 53 | + echo 0 && exit 0 |
| 54 | + fi |
| 55 | + |
| 56 | + # If one of the pre-release identifiers is empty, the one with the non-empty identifier has lower precedence |
| 57 | + if [ -z "$1" ]; then |
| 58 | + echo 1 && exit 0 |
| 59 | + elif [ -z "$2" ]; then |
| 60 | + echo -1 && exit 0 |
| 61 | + fi |
| 62 | + |
| 63 | + # Split pre-release identifiers into individual parts |
| 64 | + IFS='.' read -r -a parts1 <<<"$1" |
| 65 | + IFS='.' read -r -a parts2 <<<"$2" |
| 66 | + |
| 67 | + # Compare each part of the pre-release identifiers |
| 68 | + for ((i = 0; i < ${#parts1[@]} || i < ${#parts2[@]}; i++)); do |
| 69 | + part1="${parts1[i]:=""}" |
| 70 | + part2="${parts2[i]:=""}" |
| 71 | + |
| 72 | + # If one part is missing, it has lower precedence |
| 73 | + if [ -z "$part1" ]; then |
| 74 | + echo -1 && exit 0 |
| 75 | + elif [ -z "$part2" ]; then |
| 76 | + echo 1 && exit 0 |
| 77 | + fi |
| 78 | + |
| 79 | + # If parts are numeric, compare numerically |
| 80 | + if [[ "$part1" =~ ^[0-9]+$ ]] && [[ "$part2" =~ ^[0-9]+$ ]]; then |
| 81 | + compare_components "$part1" "$part2" |
| 82 | + fi |
| 83 | + |
| 84 | + # Lexicographically compare non-numeric parts |
| 85 | + [[ "$part1" < "$part2" ]] && echo -1 && exit 0 |
| 86 | + [[ "$part1" > "$part2" ]] && echo 1 && exit 0 |
| 87 | + done |
| 88 | + |
| 89 | + # Versions are equal |
| 90 | + echo 0 |
| 91 | +} |
| 92 | + |
| 93 | +# Function to compare two semver strings. |
| 94 | +# Arguments: |
| 95 | +# $1: First semver string |
| 96 | +# $2: Second semver string |
| 97 | +# Output: |
| 98 | +# -1 if the first version is less than the second version |
| 99 | +# 0 if the versions are equal |
| 100 | +# 1 if the first version is greater than the second version |
| 101 | +compare_semver() { |
| 102 | + # Validate the semver strings |
| 103 | + validate_semver "$1" |
| 104 | + validate_semver "$2" |
| 105 | + |
| 106 | + # Remove build metadata if present |
| 107 | + semver1=$(cut -d+ -f1 <<<"$1") |
| 108 | + semver2=$(cut -d+ -f1 <<<"$2") |
| 109 | + |
| 110 | + # Extract major, minor, patch, and pre-release versions from the input strings |
| 111 | + major1=$(cut -d. -f1 <<<"$semver1") |
| 112 | + minor1=$(cut -d. -f2 <<<"$semver1") |
| 113 | + patch1=$(cut -d. -f3 -s <<<"$semver1" | cut -d- -f1) |
| 114 | + prerelease1=$(cut -d- -f2- -s <<<"$semver1") |
| 115 | + |
| 116 | + major2=$(cut -d. -f1 <<<"$semver2") |
| 117 | + minor2=$(cut -d. -f2 <<<"$semver2") |
| 118 | + patch2=$(cut -d. -f3 -s <<<"$semver2" | cut -d- -f1) |
| 119 | + prerelease2=$(cut -d- -f2- -s <<<"$semver2") |
| 120 | + |
| 121 | + # Compare each component of the versions |
| 122 | + compare_components "$major1" "$major2" |
| 123 | + compare_components "$minor1" "$minor2" |
| 124 | + compare_components "$patch1" "$patch2" |
| 125 | + |
| 126 | + # Check if one version has a pre-release identifier while the other does not |
| 127 | + if [ -n "$prerelease1" ] && [ -z "$prerelease2" ]; then |
| 128 | + echo -1 && exit 0 |
| 129 | + elif [ -z "$prerelease1" ] && [ -n "$prerelease2" ]; then |
| 130 | + echo 1 && exit 0 |
| 131 | + fi |
| 132 | + |
| 133 | + # Compare pre-release identifiers if they exist |
| 134 | + if [ -n "$prerelease1" ] && [ -n "$prerelease2" ]; then |
| 135 | + compare_prerelease "$prerelease1" "$prerelease2" && exit 0 |
| 136 | + fi |
| 137 | + |
| 138 | + # Versions are equal |
| 139 | + echo 0 |
| 140 | +} |
| 141 | + |
| 142 | +compare_semver "$1" "$2" |
0 commit comments