Bash:从给定范围获取最新的 semver - 同时减少冗长的条件逻辑

Bash: Obtain latest semver from a given range - whilst reducing verbose conditional logic

背景

我正在开发一个 bash 脚本来从 semver 个版本的数组中找到给定范围内的最新版本。

(example.sh) 下面的 code/gist 包含一个名为 log_latest_version_in_range 的函数,它接受一个参数。参数指定包名称和 semver range。以下是调用该函数的几个示例:

# passing one comparator.
log_latest_version_in_range "quxx@\<=1.0.1"

# passing two comparators.
log_latest_version_in_range "quxx@\>=0.1.0\ \<0.2.6"

调用该函数类似于您可能如何为特定 semver 范围内的包 运行 npm install。传递给函数的参数包括:

注;该参数包括 \ 用于转义目的以避免分词。但这有点无关紧要,它们将存在于参数中并且无法更改。

当前形式的

example.sh 成功地 produce/log 了预期的结果。


问题

为了创建 versions_in_range 数组,程序在 VERSIONS 数组上循环,并执行大量条件分支以确定版本是否在范围内 (从行:43)。如果可能的话,我想重构它。在 bash?

中是否有更简洁的方法来实现这一点?

example.sh

#!/usr/bin/env bash

declare -ra VERSIONS=(\
    0.1.0 \
    0.1.1 \
    0.1.2 \
    0.2.0 \
    0.2.1 \
    0.2.2 \
    0.2.3 \
    0.2.4 \
    0.2.5 \
    0.2.6 \
    0.2.7 \
    0.2.8 \
    0.2.9 \
    0.2.10 \
    0.2.11 \
    0.2.12 \
    0.2.13 \
    0.2.14 \
    0.2.15)

log_latest_version_in_range () {
  local pkg_name comparator_count
  pkg_name=$(sed "s/@.*//" <<< "")
  comparator_count=$(awk -F " " '{ print NF }' <<< "")

  if [[ comparator_count -gt 2 ]]; then
    printf "%b" "Cannot specify greater than two comparators\n"
    return
  fi

  # Get operator and version for each comparator in comparator set.
  local opr1 ver1 opr2 ver2
  opr1=$( sed "s/.*@//; s/\\//g; s/ .*//; s/[^>=<].*//" <<< "")
  ver1=$( sed "s/.*@//; s/\\//g; s/ .*//; s/.*[>=<]//" <<< "")
  opr2=$( sed "s/.*@//; s/\\//g; s/.* //; s/[^>=<].*//" <<< "")
  ver2=$( sed "s/.*@//; s/\\//g; s/.* //; s/.*[>=<]//" <<< "")

  # Create an array of all versions that are within range.
  local -a versions_in_range
  for v in "${VERSIONS[@]}"; do
    if [ "$comparator_count" == "1" ]; then
      if [ "$opr1" == ">=" ] && is_ge "$v" "$ver1"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == ">" ] && is_gt "$v" "$ver1"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == "<=" ] && is_le "$v" "$ver1"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == "<" ] && is_lt "$v" "$ver1"; then
        versions_in_range+=("$v")
      fi
    elif [ "$comparator_count" == "2" ]; then
      if [ "$opr1" == ">=" ] && [ "$opr2" == "<" ]\
          && is_ge "$v" "$ver1" && is_lt "$v" "$ver2"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == ">=" ] && [ "$opr2" == "<=" ]\
          && is_ge "$v" "$ver1" && is_le "$v" "$ver2"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == "<" ] && [ "$opr2" == ">=" ]\
          && is_lt "$v" "$ver1" && is_ge "$v" "$ver2"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == "<" ] && [ "$opr2" == ">" ]\
          && is_lt "$v" "$ver1" && is_gt "$v" "$ver2"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == "<=" ] && [ "$opr2" == ">" ]\
          && is_le "$v" "$ver1" && is_gt "$v" "$ver2"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == "<=" ] && [ "$opr2" == ">=" ]\
          && is_le "$v" "$ver1" && is_ge "$v" "$ver2"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == ">" ] && [ "$opr2" == "<" ]\
          && is_gt "$v" "$ver1" && is_lt "$v" "$ver2"; then
        versions_in_range+=("$v")
      elif [ "$opr1" == ">" ] && [ "$opr2" == "<=" ]\
          && is_gt "$v" "$ver1" && is_le "$v" "$ver2"; then
        versions_in_range+=("$v")
      fi
    fi
  done

  # npm registry may not have version available within the range.
  if [ "${#versions_in_range[@]}" == 0 ]; then
    local mssg="Version cannot be found within range\n"
    printf "%b" "$mssg"
    return 0
  fi

  # Get the last/latest version in range
  local latest_version="${versions_in_range[${#versions_in_range[@]}-1]}"

  printf "%b" "$latest_version\n"
}

is_lt () {
  test "$(echo "$@" | tr " " "\n" | sort -t. -k1,1nr -k2,2nr -k3,3nr -k4,4nr\
      | head -n 1)" != ""
}

is_gt () {
  test "$(echo "$@" | tr " " "\n" | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n\
      | head -n 1)" != ""
}

is_le () {
  test "$(echo "$@" | tr " " "\n" | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n\
      | head -n 1)" == ""
}

is_ge () {
  test "$(echo "$@" | tr " " "\n" | sort -t. -k1,1nr -k2,2nr -k3,3nr -k4,4nr\
      | head -n 1)" == ""
}

#------------------------------------------------------------------------------
# TESTING
#------------------------------------------------------------------------------

# Passing two comparators
printf "%b" "======= Two Comparators args ========\n"
log_latest_version_in_range "quxx@\>=0.1.0\ \<0.2.6"   # --> 0.2.5
log_latest_version_in_range "quxx@\>=0.2.8\ \<=0.2.13" # --> 0.2.13
log_latest_version_in_range "quxx@\<0.2.11\ \>=0.2.8"  # --> 0.2.10
log_latest_version_in_range "quxx@\<0.2.15\ \>0.2.8"   # --> 0.2.14
log_latest_version_in_range "quxx@\<=0.2.15\ \>0.2.8"  # --> 0.2.15
log_latest_version_in_range "quxx@\<=0.2.0\ \>=0.1.0"  # --> 0.2.0
log_latest_version_in_range "quxx@\>0.2.0\ \<0.2.6"    # --> 0.2.5
log_latest_version_in_range "quxx@\>0.2.0\ \<=0.2.6"   # --> 0.2.6

printf "%b" "======== One Comparator arg =========\n"
log_latest_version_in_range "quxx@\>=0.2.10"  # --> 0.2.15
log_latest_version_in_range "quxx@\>0.2.10"   # --> 0.2.15
log_latest_version_in_range "quxx@\<=0.2.10"  # --> 0.2.10
log_latest_version_in_range "quxx@\<0.2.10"   # --> 0.2.9

printf "%b" "=============== Other ==============\n"
log_latest_version_in_range "quxxx@\>0.2.14\ \<0.2.15\ \<=1.0.0"
    # --> Cannot specify greater than two comparators

log_latest_version_in_range "quxx@\>0.2.14\ \<0.2.15"
    # --> Version cannot be found within range

来自运行宁example.sh的正确output/log如下:

======= Two Comparators args ========

0.2.5

0.2.13

0.2.10

0.2.14

0.2.15

0.2.0

0.2.5

0.2.6

======== One Comparator arg =========

0.2.15

0.2.15

0.2.10

0.2.9

=============== Other ==============

Cannot specify greater than two comparators

Version cannot be found within range

"I'd like to refactor this if possible. Is there a more terse way to achieve that in bash?"

我最终选择重构 versions_in_range 数组的值是如何通过使用具有多个条件的 if 语句来确定的。

总体而言,这减少了 elif ...; then 语句的使用,进而导致 versions_in_range+=("$v").

的实例减少

重构后的片段 (从第 43 行开始):

# Create an array of all versions that are within range.
local -a versions_in_range
for v in "${VERSIONS[@]}"; do
  if [ "$comparator_count" == "1" ]; then
    if ( [ "$opr1" == ">=" ] && is_ge "$v" "$ver1" ) ||
       ( [ "$opr1" == ">"  ] && is_gt "$v" "$ver1" ) ||
       ( [ "$opr1" == "<=" ] && is_le "$v" "$ver1" ) ||
       ( [ "$opr1" == "<"  ] && is_lt "$v" "$ver1" ); then
      versions_in_range+=("$v")
    fi
  elif [ "$comparator_count" == "2" ]; then
    if ( [ "$opr1" == ">=" ] && [ "$opr2" == "<" ] &&
          is_ge "$v" "$ver1" && is_lt "$v" "$ver2" ) ||
       ( [ "$opr1" == ">=" ] && [ "$opr2" == "<=" ] &&
          is_ge "$v" "$ver1" && is_le "$v" "$ver2" ) ||
       ( [ "$opr1" == "<"  ] && [ "$opr2" == ">=" ] &&
          is_lt "$v" "$ver1" && is_ge "$v" "$ver2" ) ||
       ( [ "$opr1" == "<"  ] && [ "$opr2" == ">" ] &&
          is_lt "$v" "$ver1" && is_gt "$v" "$ver2" ) ||
       ( [ "$opr1" == "<=" ] && [ "$opr2" == ">" ] &&
          is_le "$v" "$ver1" && is_gt "$v" "$ver2" ) ||
       ( [ "$opr1" == "<=" ] && [ "$opr2" == ">=" ] &&
          is_le "$v" "$ver1" && is_ge "$v" "$ver2" ) ||
       ( [ "$opr1" == ">"  ] && [ "$opr2" == "<" ] &&
          is_gt "$v" "$ver1" && is_lt "$v" "$ver2" ) ||
       ( [ "$opr1" == ">"  ] && [ "$opr2" == "<=" ] &&
          is_gt "$v" "$ver1" && is_le "$v" "$ver2" ); then
      versions_in_range+=("$v")
    fi
  fi
done