Contents

以点分隔版本格式的字符串比较

1. 概述

在软件产品的版本升级过程中,我们经常执行的一项重要活动是版本比较。更重要的是,如果活动是通过 bash 脚本完成的,那么比较以点分隔的版本格式的字符串就成为一项至关重要的任务。

在本文中,我们将讨论比较以点分隔的版本格式的两个字符串以确定哪个是最新版本(即哪个版本号更大)的算法。我们还将了解其他可以帮助进行版本比较的外部实用程序(Linux 市场的一部分)。

2. 什么是版本号?

版本号是一个字符串,用于标识软件产品的唯一状态。ABCD 形式的版本号(包含数字或字符串文字形式的“A”、“B”、“C”、“D”)是主要由点分隔的数字或字符串字段的字符串。在语义版本控制 下,这些字段通常代表层次结构,其中前两个字段代表主要 (A) 和次要 (B)。第三个字段是“补丁号”(C)。最右边的字段 (D) 称为修订版,但也可以称为“内部版本”或“内部版本号”:

/uploads/compare_dot_separated_version_string/1.png

现在,问题来了:“在 Bash 中,我们有什么直接的方法可以比较点分隔版本格式的字符串——例如,2.4.5 和 2.8 和 2.4.5.1?答案是否定的,不可能直接比较它们。但是,我们可以比较固有字段以找到最新版本。

3. 不使用任何外部工具的版本比较

根据版本字符串中字段的性质,我们将说明一些在 Bash 中比较两个点分隔版本格式的字符串的解决方案。在三种不同的场景下,我们将专注于使用 bash 内置函数来解决问题。

3.1.数字字段的点分隔序列

我们可以使用printf 作为内置的 bash 来比较最多包含四个字段的版本字符串

$ function ver { printf "%03d%03d%03d%03d" $(echo "$1" | tr '.' ' '); }
$ [ $(ver 10.9) -lt $(ver 10.10) ] && echo 1
1

**递归为我们提供了另一种比较版本字符串的方法。**以下算法通过操作字符串 并在“.”上递归拆分来比较包含相等数量字段的版本字符串:

compare_versions() {
     # implementing string manipulation
     local a=${1%%.*} b=${2%%.*}
     [[ "10#${a:-0}" -gt "10#${b:-0}" ]] && return 1
     [[ "10#${a:-0}" -lt "10#${b:-0}" ]] && return 2
     # re-assigning a and b with greatest of 1 and 2 after manipulation
     a=${1:${#a} + 1}
     b=${2:${#b} + 1}
     # terminal condition for recursion
     [[ -z $a && -z $b ]] || compare_versions "$a" "$b"
}
Usage: compare_versions <ver_1> <ver_2>

如果版本 1 小于版本 2,则函数compare_versions返回 2,如果版本 1 大于版本 2,则返回 1。

现在,我们来看一个比较长度不等的版本的方法,比如 3.0002 ‘>’ 3.0003.3

vercomp() {
    if [[ $1 == $2 ]]
    then
        return 0
    fi
    local IFS=.
    local i ver1=($1) ver2=($2)
    # fill empty fields in ver1 with zeros
    for ((i=${#ver1[@]}; i<${#ver2[@]}; i++))
    do
        ver1[i]=0
    done
    for ((i=0; i<${#ver1[@]}; i++))
    do
        if [[ -z ${ver2[i]} ]]
        then
            # fill empty fields in ver2 with zeros
            ver2[i]=0
        fi
        if ((10#${ver1[i]} > 10#${ver2[i]}))
        then
            return 1
        fi
        if ((10#${ver1[i]} < 10#${ver2[i]}))
        then
            return 2
        fi
    done
    return 0
}

该算法使用填充将空字段替换为零,并比较两个版本字符串的每个字段。如果版本 1 小于版本 2,则函数vercomp返回 2,如果版本 1 大于版本 2,则返回 1。

这里, ‘。’ 是字段分隔符IFS 。 内部字段分隔符 (IFS) 是一个特殊的 shell 变量  ,用于扩展 后的分词:

testvercomp() {
    vercomp $1 $2
    case $? in
        0) op='=';;
        1) op='>';;
        2) op='<';;
    esac
    if [[ $op != $3 ]]
    then
        echo "Fail: Expected '$3', Actual '$op', Arg1 '$1', Arg2 '$2'"
    else
        echo "Pass: '$1 $op $2'"
    fi
}
 
# Run tests
# argument table format:
# testarg1   testarg2     expected_relationship
echo "The following tests should pass"
while read -r test
do
    testvercomp $test
done
$ The following tests should pass
1 1 =
Pass: '1 = 1'
2.1 2.2 <
Pass: '2.1 < 2.2'
3.0.4.10 3.0.4.2 >
Pass: '3.0.4.10 > 3.0.4.2'
3.2 3.2.1.9.8144 <
Pass: '3.2 < 3.2.1.9.8144'

函数testvercompvercomp的输出与输入进行比较,并返回“Pass”或“Fail”。

3.2. 最后一个字段可选地以字母结尾

到目前为止,我们已经讨论了比较包含点分隔数字字段序列的版本字符串的方法。但是,让我们讨论一个特殊情况,版本的最后一个字段可选地以字母结尾。本质上,我们现在将看到一种可以将 2.5 与 2.5a 进行比较的方法:

V() 
{ 
    local a=$1 op=$2 b=$3 al=${1##*.} bl=${3##*.}
    # Left-trim digits from the tail items so only letters are left
    while [[ $al =~ ^[[:digit:]] ]]; do
        al=${al:1};
    done
    while [[ $bl =~ ^[[:digit:]] ]]; do
        bl=${bl:1}
    done
    # Right trim letters from a and b to leave just the sequence of numeric items
    local ai=${a%$al} bi=${b%$bl}
    local ap=${ai//[[:digit:]]} bp=${bi//[[:digit:]]}
    # zero right-paddings
    ap=${ap//./.0} bp=${bp//./.0}
    local w=1 fmt=$a.$b x IFS=.
    for x in $fmt; do
        [ ${#x} -gt $w ] && w=${#x};
    done
    fmt=${*//[^.]} fmt=${fmt//./%${w}s}
    printf -v a $fmt $ai$bp
    printf -v a "%s-%${w}s" $a $al
    printf -v b $fmt $bi$ap
    printf -v b "%s-%${w}s" $b $bl
    case $op in
        '<='|'>=' ) [ "$a" ${op:0:1} "$b" ] || [ "$a" = "$b" ] ;;
                * ) [ "$a" $op "$b" ] ;;
    esac
}
P() { printf "[[email protected]](/cdn_cgi/l/email_protection)"; }
EXPECT() { printf "[[email protected]](/cdn_cgi/l/email_protection)"; }
CODE() { awk $BASH_LINENO'==NR{print " "$2,$3,$4}' "$0"; }

让我们看一个示例输出。注意:++(真)和__(假):

$V 3.5 '>' 3.5b && P + || P _; EXPECT _; CODE
__ 3.5 '>' 3.5b 
$V 3.0 '<' 3.0.3 && P + || P _; EXPECT +; CODE
++ 3.0 '<' 3.0.3 
$V 3.0002 '>' 3.0003.3 && P + || P _; EXPECT _; CODE
__ 3.0002 '>' 3.0003.3 
$V 3.0003 '>' 3.0000004 && P + || P _; EXPECT _; CODE
__ 3.0003 '>' 3.0000004

在这种情况下,该算法还比较两个版本,包括任意字段的位数不受限制,例如 3.0003 > 3.0000004,以及字段数量不受限制。与前一种情况不同,在这种情况下,会自动插入零来比较相同数量的字段:1.0 < 1.0.1 表示 1.0.0 < 1.0.1。

3.3. 基于元组的点分隔版本号

我们将看到,在一些特殊情况下,版本号也可能包含字母符号。这种类型的一些示例包括版本号,例如 10.c.3、4.0-RC1 和 4.0-RC2。

让我们看看如何使用ASCII 排序 在 Bash 中按字典顺序比较这些基于元组、点分隔的版本字符串:

compare-versions()
{
    if [[ $1 == $2 ]]; then
        return 0
    fi
    local IFS=.
    # Everything after the first character not in [^0-9.] is compared
    local i a=(${1%%[^0-9.]*}) b=(${2%%[^0-9.]*})
    local arem=${1#${1%%[^0-9.]*}} brem=${2#${2%%[^0-9.]*}}
    for ((i=0; i<${#a[@]} || i<${#b[@]}; i++)); do
        if ((10#${a[i]:-0} < 10#${b[i]:-0})); then
            return 2
        elif ((10#${a[i]:-0} > 10#${b[i]:-0})); then
            return 1
        fi
    done
    if [ "$arem" '<' "$brem" ]; then
        return 2
    elif [ "$arem" '>' "$brem" ]; then
        return 1
    fi
    return 0
}

函数testvercomp(第 3.1 节)将比较版本的输出与输入进行比较,并返回“通过”或“失败”。

$ The following tests should pass
1.0rc1 1.0rc2 <
Pass: '1.0rc1 < 1.0rc2'

4. 使用外部实用程序进行版本比较

接下来,让我们讨论一些未内置在 shell 中的命令或实用程序。它们为我们提供了一种相当简单的方法来比较 Bash 中以点分隔的版本字符串。

4.1. 使用GNUCoreutils-7

如果我们有coreutils-7,我们可以使用带有*-V* 选项(–version-sort)的排序命令进行比较:

$ printf '2.4.5\n2.8\n2.4.5.1\n' | sort -V
2.4.5
2.4.5.1
2.8

使用GNU sort -C或*–check=silent*,我们可以这样写:

$ verlte() { 
>     printf '%s\n%s' "$1" "$2" | sort -C -V
> }
$ verlte 2.5.7 2.5.6 && echo "yes" || echo "no"
no

另一种以点分隔的版本格式比较两个字符串(“1.2”、“2.3.4”、“1.0”和“1.10.1”)的方法需要最多三个版本字段。必须预先知道最大字段数:

$ expr $(printf "1.10.1\n1.7" | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -g | sed -n 2p) != "1.7"
1

这段 bash 代码返回 1,因为 1.10.1 大于 1.7。

好吧,如果我们知道字段的数量,我们可以使用*-kn,n*来设计另一个超级简单的解决方案:

$ printf '2.4.5\n2.8\n2.4.5.1\n2.10.2\n' | sort -t '.' -k 1,1 -k 2,2 -k 3,3 -k 4,4 -g
2.4.5
2.4.5.1
2.8
2.10.2

4.2. 使用dpkg –compare-versions

本质上,我们可以使用dpkg  在 bash 中比较两个点分隔版本格式的字符串。

Usage: dpkg --compare-versions <condition>

如果条件为真,dpkg返回的状态码 将为零(表示成功)。因此,我们可以在*“if”*语句中使用此命令来比较两个版本号:

$ if $(dpkg --compare-versions "2.11" "lt" "3"); then echo true; else echo false; fi
true

5. 比较版本的规范化技术

**在我们到目前为止讨论的算法中,有一些算法无意中使用了一种转换技术来获得一组易于比较的数字。这些产生的数字表面上被称为标准化版本号 。**为了比较点分隔的版本,我们将设计一个解决方案来规范化然后比较它们。

让我们首先将版本字符串中的八进制数转换为十进制。例如: 1.08 → 1 8, 1.0030 → 1 30, 2021-02-03 → 2021 2 3… 转换代码为:

v() { 
printf "%04d%04d%04d%04d%04d" $(for i in ${1//[^0-9]/ }; do printf "%d" $((10#$i)); done) 
}

然后,我们将它们比较为:

while read -r test; do
    set -- $test
    printf "$test "
    eval "if [[ $(v $1) $3 $(v $2) ]] ; then
              echo true
          else
              echo false
          fi"
done

让我们看一个示例输出:

$  1.08 1.0030 <
1.08 1.0030 < true

6. 使用哪一个?

我们知道 shell 内置命令执行速度很快。因此,我们在方法选择的决定中应该强烈考虑算法是否只使用这些命令。然而,一般来说,这些方法使用相当复杂的方法。

如果简单是最终的动机,那么 Linux 在它的所有库中都有一些方便的实用程序,可以难以置信地减少所需的工作量。sortdpkg是我们讨论过的几个。

尽管如此,我们想要比较的字符串版本号类型应该只控制这些标准。