semver.sh
                        
                             · 3.7 KiB · Bash
                        
                    
                    
                      
                        Raw
                      
                      
                        
                          
                        
                    
                    
                
                
            #!/bin/bash
# set -x
# Stupid Simple Semver range validator
# (no build metadata and no prereleases, and no multiple ranges (||))
# Prints a list of versions that match a semver range 
# ./semver.sh "range" version version version...
# ./semver.sh ">=0.10.32 <7" 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0 7.3.0 7.4.0 | xargs
# 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0
# ./semver.sh "^6.9.4" 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0 7.3.0 7.4.0 | xargs
# 6.9.4 6.10.0
# ./semver.sh "~6.9.4" 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0 7.3.0 7.4.0 | xargs
# 6.9.4
# TODO: error handling, validation
# Basic concept:
# 1.x.x  = ^1.0.0 = (>=1.0.0 <2.0.0)
# 1.1.x  = ~1.1.0 = (>=1.1.0 <1.2.0)
# ^1.x.x = ^1.0.0
# ~1.x.x = ~1.0.0
SEMVER_RANGE="$1"; shift
SEMVER_AVAILABLE_VERSIONS="$@"
SEMVER_RE_COMPARATOR='[<>=\^~]*'
SEMVER_RE_VERSION='[0-9x\.]+'
# Takes a range with multiple comparisons, and splits it into lines
# >=1.2.3 < 3
#    >=1.2.3
#    < 3
semver_split_range () {
  egrep -o "($SEMVER_RE_COMPARATOR\s*$SEMVER_RE_VERSION)"
}
# Hacky way to make versions comparable
# Won't work if version numbers are over 100k'
# 1.2.3 => 000001000002000003
semver_to_number () {
  tr "." " " | xargs printf "%06g"
}
# Convert a range part into a set of comparison lines processable by bc
# ^1.2.3 => >=1.2.3 <2.0.0
#    >= 000001000002000003
#    <  000002000000000000
semver_create_comparator () {
  local COMPARATOR="$(egrep -o "^$SEMVER_RE_COMPARATOR" <<< "$1")"
  local VERSION="$(egrep -o "$SEMVER_RE_VERSION$" <<< "$1")"
  local VP
  local ZERO_OR_X
  if [[ -n "$COMPARATOR" ]]; then
    ZERO_OR_X="0"
  else
    ZERO_OR_X="x"
  fi
  # Append either 0's or x's to the version, depending on comparator presence
  #  4 => 4.x.x
  # ^4 => 4.0.0
  while [[ "$(egrep -o "\." <<< "$VERSION" | wc -l)" -lt "2" ]]; do
    VERSION="$VERSION.$ZERO_OR_X"
  done
  # If comparator is not defined (1.x.x, 1.x, 1, 1.0.x, 1.0.0 etc),
  # assign one based on the number of x's
  # 1.x.x => ^  1.x.x
  # 1.0.x => ~  1.0.x
  # 1.0.0 => == 1.0.0
  if [[ -z "$COMPARATOR" ]]; then
    if [[ "$VERSION" =~ \.x\.x ]]; then
      COMPARATOR="^"
    elif [[ "$VERSION" =~ \.x ]]; then
      COMPARATOR="~"
    else
      COMPARATOR="=="
    fi
  fi
  # Finally, replace all the x's with 0s'
  # ^  1.x.x => ^ 1.0.0
  VERSION=$(sed -E 's|\.x|\.0|g' <<< "$VERSION")
  # Convert the ~,^ into proper >=, < comparator ranges
  # ^1.0.0 = (>=1.0.0 <2.0.0)
  # ~1.0.0 = (>=1.0.0 <1.1.0)
  if [[ "$COMPARATOR" =~ \^|~ ]]; then
    IFS="." read -ra VP <<< "$VERSION"
    if [[ "$COMPARATOR" == "~" ]]; then
      # Up the minor; 0 the patch
      VP[1]=$(( ${VP[1]} + 1 ))
      VP[2]=0
    else
      # Up the major; 0 the minor and patch
      VP[0]=$(( ${VP[0]} + 1 ))
      VP[1]=0
      VP[2]=0
    fi
    COMPARATOR=">="
    echo "< $(semver_to_number <<< "${VP[0]}.${VP[1]}.${VP[2]}")"
  fi
  echo "$COMPARATOR $(semver_to_number <<< "$VERSION")"
}
# Create all of the comparator lines processable by bc for the entire semver range
# TODO: make it break for invalid ranges
semver_create_comparators () {
  local R
  while read R; do
    semver_create_comparator "$R"
  done < <(semver_split_range <<< "$SEMVER_RANGE")
}
# Run a semver against all comparisons
# If all conditions pass, return success
semver_compare () {
  local C
  while read C; do
    [[ "$MATCH" == "false" ]] && break
    if [[ "$(bc <<< "$1 $C")" == "0" ]]; then
      return 1
    fi
  done < <(echo -e "$V_COMPARATORS")
}
# Precompute the range into bc-processable comparator strings
V_COMPARATORS="$(semver_create_comparators)"
# Compare each provided version against the provided range, and print
# the ones that succeed
for A in $SEMVER_AVAILABLE_VERSIONS; do
  semver_compare "$(semver_to_number <<< "$A")" && echo $A
done
                | 1 | #!/bin/bash | 
| 2 | |
| 3 | # set -x | 
| 4 | |
| 5 | # Stupid Simple Semver range validator | 
| 6 | # (no build metadata and no prereleases, and no multiple ranges (||)) | 
| 7 | |
| 8 | # Prints a list of versions that match a semver range | 
| 9 | # ./semver.sh "range" version version version... | 
| 10 | |
| 11 | # ./semver.sh ">=0.10.32 <7" 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0 7.3.0 7.4.0 | xargs | 
| 12 | # 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0 | 
| 13 | |
| 14 | # ./semver.sh "^6.9.4" 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0 7.3.0 7.4.0 | xargs | 
| 15 | # 6.9.4 6.10.0 | 
| 16 | |
| 17 | # ./semver.sh "~6.9.4" 0.10.32 6.7.0 6.9.2 6.9.4 6.10.0 7.3.0 7.4.0 | xargs | 
| 18 | # 6.9.4 | 
| 19 | |
| 20 | # TODO: error handling, validation | 
| 21 | |
| 22 | # Basic concept: | 
| 23 | # 1.x.x = ^1.0.0 = (>=1.0.0 <2.0.0) | 
| 24 | # 1.1.x = ~1.1.0 = (>=1.1.0 <1.2.0) | 
| 25 | # ^1.x.x = ^1.0.0 | 
| 26 | # ~1.x.x = ~1.0.0 | 
| 27 | |
| 28 | SEMVER_RANGE="$1"; shift | 
| 29 | SEMVER_AVAILABLE_VERSIONS="$@" | 
| 30 | |
| 31 | SEMVER_RE_COMPARATOR='[<>=\^~]*' | 
| 32 | SEMVER_RE_VERSION='[0-9x\.]+' | 
| 33 | |
| 34 | # Takes a range with multiple comparisons, and splits it into lines | 
| 35 | # >=1.2.3 < 3 | 
| 36 | # >=1.2.3 | 
| 37 | # < 3 | 
| 38 | semver_split_range () { | 
| 39 | egrep -o "($SEMVER_RE_COMPARATOR\s*$SEMVER_RE_VERSION)" | 
| 40 | } | 
| 41 | |
| 42 | # Hacky way to make versions comparable | 
| 43 | # Won't work if version numbers are over 100k' | 
| 44 | # 1.2.3 => 000001000002000003 | 
| 45 | semver_to_number () { | 
| 46 | tr "." " " | xargs printf "%06g" | 
| 47 | } | 
| 48 | |
| 49 | # Convert a range part into a set of comparison lines processable by bc | 
| 50 | # ^1.2.3 => >=1.2.3 <2.0.0 | 
| 51 | # >= 000001000002000003 | 
| 52 | # < 000002000000000000 | 
| 53 | semver_create_comparator () { | 
| 54 | local COMPARATOR="$(egrep -o "^$SEMVER_RE_COMPARATOR" <<< "$1")" | 
| 55 | local VERSION="$(egrep -o "$SEMVER_RE_VERSION$" <<< "$1")" | 
| 56 | local VP | 
| 57 | |
| 58 | local ZERO_OR_X | 
| 59 | if [[ -n "$COMPARATOR" ]]; then | 
| 60 | ZERO_OR_X="0" | 
| 61 | else | 
| 62 | ZERO_OR_X="x" | 
| 63 | fi | 
| 64 | |
| 65 | # Append either 0's or x's to the version, depending on comparator presence | 
| 66 | # 4 => 4.x.x | 
| 67 | # ^4 => 4.0.0 | 
| 68 | while [[ "$(egrep -o "\." <<< "$VERSION" | wc -l)" -lt "2" ]]; do | 
| 69 | VERSION="$VERSION.$ZERO_OR_X" | 
| 70 | done | 
| 71 | |
| 72 | # If comparator is not defined (1.x.x, 1.x, 1, 1.0.x, 1.0.0 etc), | 
| 73 | # assign one based on the number of x's | 
| 74 | # 1.x.x => ^ 1.x.x | 
| 75 | # 1.0.x => ~ 1.0.x | 
| 76 | # 1.0.0 => == 1.0.0 | 
| 77 | if [[ -z "$COMPARATOR" ]]; then | 
| 78 | if [[ "$VERSION" =~ \.x\.x ]]; then | 
| 79 | COMPARATOR="^" | 
| 80 | elif [[ "$VERSION" =~ \.x ]]; then | 
| 81 | COMPARATOR="~" | 
| 82 | else | 
| 83 | COMPARATOR="==" | 
| 84 | fi | 
| 85 | fi | 
| 86 | |
| 87 | # Finally, replace all the x's with 0s' | 
| 88 | # ^ 1.x.x => ^ 1.0.0 | 
| 89 | VERSION=$(sed -E 's|\.x|\.0|g' <<< "$VERSION") | 
| 90 | |
| 91 | # Convert the ~,^ into proper >=, < comparator ranges | 
| 92 | # ^1.0.0 = (>=1.0.0 <2.0.0) | 
| 93 | # ~1.0.0 = (>=1.0.0 <1.1.0) | 
| 94 | if [[ "$COMPARATOR" =~ \^|~ ]]; then | 
| 95 | IFS="." read -ra VP <<< "$VERSION" | 
| 96 | if [[ "$COMPARATOR" == "~" ]]; then | 
| 97 | # Up the minor; 0 the patch | 
| 98 | VP[1]=$(( ${VP[1]} + 1 )) | 
| 99 | VP[2]=0 | 
| 100 | else | 
| 101 | # Up the major; 0 the minor and patch | 
| 102 | VP[0]=$(( ${VP[0]} + 1 )) | 
| 103 | VP[1]=0 | 
| 104 | VP[2]=0 | 
| 105 | fi | 
| 106 | COMPARATOR=">=" | 
| 107 | echo "< $(semver_to_number <<< "${VP[0]}.${VP[1]}.${VP[2]}")" | 
| 108 | fi | 
| 109 | |
| 110 | echo "$COMPARATOR $(semver_to_number <<< "$VERSION")" | 
| 111 | } | 
| 112 | |
| 113 | # Create all of the comparator lines processable by bc for the entire semver range | 
| 114 | # TODO: make it break for invalid ranges | 
| 115 | semver_create_comparators () { | 
| 116 | local R | 
| 117 | |
| 118 | while read R; do | 
| 119 | semver_create_comparator "$R" | 
| 120 | done < <(semver_split_range <<< "$SEMVER_RANGE") | 
| 121 | } | 
| 122 | |
| 123 | # Run a semver against all comparisons | 
| 124 | # If all conditions pass, return success | 
| 125 | semver_compare () { | 
| 126 | local C | 
| 127 | |
| 128 | while read C; do | 
| 129 | [[ "$MATCH" == "false" ]] && break | 
| 130 | if [[ "$(bc <<< "$1 $C")" == "0" ]]; then | 
| 131 | return 1 | 
| 132 | fi | 
| 133 | done < <(echo -e "$V_COMPARATORS") | 
| 134 | } | 
| 135 | |
| 136 | # Precompute the range into bc-processable comparator strings | 
| 137 | V_COMPARATORS="$(semver_create_comparators)" | 
| 138 | |
| 139 | # Compare each provided version against the provided range, and print | 
| 140 | # the ones that succeed | 
| 141 | for A in $SEMVER_AVAILABLE_VERSIONS; do | 
| 142 | semver_compare "$(semver_to_number <<< "$A")" && echo $A | 
| 143 | done |