如何在Bash中比较两个以点分隔的版本格式的字符串?

275

是否有办法在Bash中比较这样的字符串,例如:2.4.52.8以及2.4.5.1


https://github.com/awslabs/amazon-eks-ami/blob/master/files/bin/vercmp - guettli
38个回答

275

这里是一个纯Bash版本,不需要任何外部工具:

#!/bin/bash
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
}

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 << EOF
1            1            =
2.1          2.2          <
3.0.4.10     3.0.4.2      >
4.08         4.08.01      <
3.2.1.9.8144 3.2          >
3.2          3.2.1.9.8144 <
1.2          2.1          <
2.1          1.2          >
5.6.7        5.6.7        =
1.01.1       1.1.1        =
1.1.1        1.01.1       =
1            1.0          =
1.0          1            =
1.0.2.0      1.0.2        =
1..0         1.0          =
1.0          1..0         =
EOF

echo "The following test should fail (test the tester)"
testvercomp 1 1 '>'

运行测试:

$ . ./vercomp
The following tests should pass
Pass: '1 = 1'
Pass: '2.1 < 2.2'
Pass: '3.0.4.10 > 3.0.4.2'
Pass: '4.08 < 4.08.01'
Pass: '3.2.1.9.8144 > 3.2'
Pass: '3.2 < 3.2.1.9.8144'
Pass: '1.2 < 2.1'
Pass: '2.1 > 1.2'
Pass: '5.6.7 = 5.6.7'
Pass: '1.01.1 = 1.1.1'
Pass: '1.1.1 = 1.01.1'
Pass: '1 = 1.0'
Pass: '1.0 = 1'
Pass: '1.0.2.0 = 1.0.2'
Pass: '1..0 = 1.0'
Pass: '1.0 = 1..0'
The following test should fail (test the tester)
FAIL: Expected '>', Actual '=', Arg1 '1', Arg2 '1'

3
请问这段代码的许可证明确吗?代码看起来很完美,但我不确定能否在 AGPLv3 许可的项目中使用它。 - Kamil Dziedzic
4
此页面底部(以及大多数其他页面)列出了许可条款。 - Dennis Williamson
4
请不要将其用于软件或文档,因为它与GNU GPL不兼容。但是对于出色的代码给一个+1。原文链接:https://www.gnu.org/licenses/license-list.html#ccbysa - Kamil Dziedzic
3
这个比较失败,'1.4rc2 > 1.3.3'。请注意,这是字母数字混合版本号。 - Salimane Adjao Moustapha
1
@SalimaneAdjaoMoustapha:它没有设计来处理那种类型的版本字符串。我没有看到其他答案可以处理那个比较。 - Dennis Williamson
显示剩余10条评论

228

如果您使用的是Ubuntu Karmic中的coreutils-7(而不是Jaunty),那么您的sort命令应该有一个-V选项(版本排序),您可以使用它来进行比较:

verlte() {
    [  "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ]
}

verlt() {
    [ "$1" = "$2" ] && return 1 || verlte $1 $2
}

verlte 2.5.7 2.5.6 && echo "yes" || echo "no" # no
verlt 2.4.10 2.4.9 && echo "yes" || echo "no" # no
verlt 2.4.8 2.4.10 && echo "yes" || echo "no" # yes
verlte 2.5.6 2.5.6 && echo "yes" || echo "no" # yes
verlt 2.5.6 2.5.6 && echo "yes" || echo "no" # no

由于GNU sort还具有-C--check=silent),它仅报告(通过退出状态)输入是否已排序,因此我们实际上不需要捕获输出并测试它-我们可以编写

verlte() {
    printf '%s\n' "$1" "$2" | sort -C -V
}

verlt() {
    ! verlte "$2" "$1"
}

我们可以将这种技术扩展到确定一个版本是否在由两个(包括)限制指定的范围内:
ver_between() {
    # args: min, actual, max
    printf '%s\n' "$@" | sort -C -V
}

7
不错的解决方案。对于Mac OSX用户,您可以使用GNU Coreutils gsort。这可以通过homebrew获取:brew install coreutils。然后上述内容只需修改以使用gsort即可。 - justsee
4
在嵌入式Linux系统中,例如在Busybox上无法工作,因为Busyboxsort没有-V选项。请注意,此处仅进行翻译,不包括解释或其他内容。 - Craig McQueen
8
使用printf比使用echo -e更好。 - phk
18
GNU sort 命令也有 -C--check=silent 选项,因此可以编写 verlte() { printf '%s\n%s' "$1" "$2" | sort -C -V } 来实现。严格小于的检查可以更简单地使用 verlt() { ! verlte "$2" "$1" } 来完成。 - Toby Speight
3
反引号不是一种过时的语法吗? - Martynas Jusevičius
显示剩余9条评论

88

实现这个目标可能没有普遍正确的方法。如果您正在尝试比较Debian软件包系统中的版本,请尝试使用dpkg --compare-versions <first> <relation> <second>命令。


18
用法:dpkg --compare-versions "1.0" "lt" "1.2" 表示 1.0 小于 1.2。比较结果 $? 如果为 0 则表示为真,因此您可以在 if 语句之后直接使用它。 - KrisWebDev

79

GNU sort有一个选项可以对版本号进行排序:

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

给:

2.4.5
2.4.5.1
2.8

而且结合使用-C,我们能够比较版本(感谢@WolframRösler的评论)。
if ! printf '7.18\n%s\n' "$(curl -V | grep -io "[0-9][0-9a-z.-]*" | head -n1)" | sort -V -C; then
  echo "Error: curl version is older than 7.18!"
else
  echo "curl version is at least 7.18."
fi

解释:

  • -C 检查获取的行是否已经排序,如果没有排序则返回退出状态 1(= 版本不相等或较旧)
  • grep -io "[0-9][0-9a-z.-]*" | head -n1 捕获由 command -V 返回的第一个版本字符串,该字符串必须以数字开头,可以后跟字母数字字符、点和连字符,甚至可以包括像 6.2-RC3 这样的版本

2
这个问题似乎是关于版本排序的。考虑使用以下命令:echo -e "2.4.10\n2.4.9" | sort -n -t. - kanaka
2
按数字排序并不正确。您需要至少先对字符串进行规范化处理。 - frankc
6
在嵌入式 Linux 系统中,例如 Busybox,无法使用此功能,因为 Busybox 的 sort 命令(http://www.busybox.net/downloads/BusyBox.html#sort)不包含 -V 选项。请注意,该选项很重要。 - Craig McQueen
2
@CraigMcQueen,最新的busybox sort已经支持选项-V - Bruce
2
好的答案,你可以添加-C来进行比较而不是排序,例如 printf '2.4.5\n2.4.10\n' | sort -V -C。如果第一个版本号较旧,则输出为空,退出状态为0;如果第一个版本号较新,则退出状态为1。 - Wolfram Rösler
显示剩余3条评论

76
function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }

用法如下:

if [ $(version $VAR) -ge $(version "6.2.0") ]; then
    echo "Version is up to date"
fi

(来自https://apple.stackexchange.com/a/123408/11374

注意:此函数目前支持最多四个组件的版本。如果需要,可以轻松扩展。


8
这种方法比上面提到的默认bash printf好得多。它能够正确处理像“1.09”这样的版本号,而正常的printf无法处理,因为“09不是一个正确的数字”。它还可以自动删除前导零,这非常有用,因为有时前导零可能会导致比较错误。 - Oleksii Chekulaiev
1
这个解决方案值得更多的点赞!感谢提供这个解决方案! - Michael Aicher
1
这是我心目中的赢家。 - n8jadams
1
你好@yairchu,感谢您的解决方案。但是,如果版本中有5个字段怎么办?这个方法是否适用? - Preeti
它在 Ash 中也可以工作。 - Mitar
显示剩余2条评论

49
如果您知道字段数量,可以使用-k n,n来得到一个超级简单的解决方案。
echo '2.4.5
2.8
2.4.5.1
2.10.2' | 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

8
我虽然晚了四年才来参加派对,但这是我迄今为止最喜欢的解决方案 :) - LOAS
是的,-t选项只接受单个字符制表符...否则,2.4-r9也可以工作。真遗憾 :/ - scottysseus
3
为了与Solaris兼容,我不得不将“-g”更改为“-n”。这个例子为什么不能这样做呢?另外,要执行“大于”类型的比较,您可以检查所需排序是否与实际排序相同...例如:desired="1.9\n1.11"; actual="$(echo -e $desired |sort -t '.' -k 1,1 -k 2,2 -g)"; ,然后验证 if [ "$desired" = "$actual" ] - tresf

24

这个版本最多支持4个字段。

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

3
如果版本号也可能有5个字段,那么可以通过以下方式使上述命令更加安全:printf "%03d%03d%03d%03d%03d" $(echo "$1" | tr '.' '\n' | head -n 5) - robinst
2
不确定是否适用于所有版本的bash,但在我的情况下,最后一个圆括号后缺少了一个分号。 - Holger Brandl
1
@robinst 为了让 head -n 起作用,我不得不改成 tr '.' '\n' - Victor Sergienko
1
稍微更好的方法是:使用命令“tr -cs '0-9' ' '”来清除并分离连字符、逗号和其他分隔符。 - Otheus
1
请注意,您可以(或必须)将“位置填充”%03d调整为%04d%02d。因此,您可以通过%03d%02d%03d来处理类似于“96.0.4664.45”的Google Chrome版本。 - hh skladby
显示剩余8条评论

13
  • 函数V - 一个纯bash解决方案,不需要外部工具。
  • 支持 = == != < <= >>=(词典排序)。
  • 可选尾字符比较:1.5a < 1.5b
  • 不等长度比较:1.6 > 1.5b
  • 从左到右读取: if V 1.5 '<' 1.6; then ....

<>

# Sample output
# Note: ++ (true) and __ (false) mean that V works correctly.

++ 3.6 '>' 3.5b
__ 2.5.7 '<=' 2.5.6
++ 2.4.10 '<' 2.5.9
__ 3.0002 '>' 3.0003.3
++ 4.0-RC2 '>' 4.0-RC1
<>
function V() # $1-a $2-op $3-$b
# Compare a and b as version strings. Rules:
# R1: a and b : dot-separated sequence of items. Items are numeric. The last item can optionally end with letters, i.e., 2.5 or 2.5a.
# R2: Zeros are automatically inserted to compare the same number of items, i.e., 1.0 < 1.0.1 means 1.0.0 < 1.0.1 => yes.
# R3: op can be '=' '==' '!=' '<' '<=' '>' '>=' (lexicographic).
# R4: Unrestricted number of digits of any item, i.e., 3.0003 > 3.0000004.
# R5: Unrestricted number of items.
{
  local a=$1 op=$2 b=$3 al=${1##*.} bl=${3##*.}
  while [[ $al =~ ^[[:digit:]] ]]; do al=${al:1}; done
  while [[ $bl =~ ^[[:digit:]] ]]; do bl=${bl:1}; done
  local ai=${a%$al} bi=${b%$bl}

  local ap=${ai//[[:digit:]]} bp=${bi//[[:digit:]]}
  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
}

代码解析

第一行:定义本地变量:

  • aopb - 比较操作数和运算符,即"3.6" > "3.5a"。
  • albl - ab 的字母尾部,初始化为尾部项,即"6"和"5a"。

第2、3行:从尾部项目左侧修剪数字,只留下字母(如果有),即 "" 和"a"。

第4行:从 ab 中删去字母以保留数字序列,作为本地变量 aibi,即"3.6" 和"3.5"。值得注意的是:"4.01-RC2" > "4.01-RC1" 会产生 ai="4.01" al="-RC2" 和 bi="4.01" bl="-RC1"。

第6行:定义本地变量:

  • apbp - 用于将零右填充到 aibi 的变量。首先仅保留 ab 的元素数量相等的各个项目之间的点。

第7行:然后在每个点后附加 "0" 以形成填充掩码。

第9行:本地变量:

  • w - 项宽度
  • fmt - printf 格式字符串,将被计算
  • x - 临时变量
  • 使用 IFS=. 命令将变量值在 '.' 处分割。

第10行:计算最大项宽度 w,用于对字典序进行比较。在我们的示例中,w=2。

第11行:通过将 $a.$b 的每个字符替换为 %${w}s 来创建 printf 对齐格式,即"3.6" > "3.5a" 会产生 "%2s%2s%2s%2s"。

第12行:"printf -v a" 设置变量 a 的值。这相当于许多编程语言中的 a=sprintf(...)。请注意,在这里,由于 IFS=. 的作用,printf 的参数会被拆分成单独的项目。

使用第一个 printf,将 a 的前几个项目用空格填充,同时从 bp 添加足够的“0”项目以确保生成的字符串 a 可以与类似格式的 b 进行比较。

请注意,我们附加的是 bp,而不是 apai,因为 apbp 可能具有不同的长度,因此这会使得 ab 具有相等的长度。

通过第二个 printf,我们附加了字母部分的 ala 中,具有足够的填充来启用有意义的比较。现在,a 已经准备好与 b 进行比较。

第 13 行:对于 b,与第 12 行相同。

第 15 行:将比较情况分为非内置 (<=>=) 和内置操作符。

第 16 行:如果比较运算符是 <=,则测试 a<b 或 a=b - 分别是 >= 的情况下 a<b 或 a=b

第 17 行:测试内置比较运算符。

<>

# All tests

function P { printf "$@"; }
function EXPECT { printf "$@"; }
function CODE { awk $BASH_LINENO'==NR{print " "$2,$3,$4}' "$0"; }
P 'Note: ++ (true) and __ (false) mean that V works correctly.\n'

V 2.5    '!='  2.5      && P + || P _; EXPECT _; CODE
V 2.5    '='   2.5      && P + || P _; EXPECT +; CODE
V 2.5    '=='  2.5      && P + || P _; EXPECT +; CODE

V 2.5a   '=='  2.5b     && P + || P _; EXPECT _; CODE
V 2.5a   '<'   2.5b     && P + || P _; EXPECT +; CODE
V 2.5a   '>'   2.5b     && P + || P _; EXPECT _; CODE
V 2.5b   '>'   2.5a     && P + || P _; EXPECT +; CODE
V 2.5b   '<'   2.5a     && P + || P _; EXPECT _; CODE
V 3.5    '<'   3.5b     && P + || P _; EXPECT +; CODE
V 3.5    '>'   3.5b     && P + || P _; EXPECT _; CODE
V 3.5b   '>'   3.5      && P + || P _; EXPECT +; CODE
V 3.5b   '<'   3.5      && P + || P _; EXPECT _; CODE
V 3.6    '<'   3.5b     && P + || P _; EXPECT _; CODE
V 3.6    '>'   3.5b     && P + || P _; EXPECT +; CODE
V 3.5b   '<'   3.6      && P + || P _; EXPECT +; CODE
V 3.5b   '>'   3.6      && P + || P _; EXPECT _; CODE

V 2.5.7  '<='  2.5.6    && P + || P _; EXPECT _; CODE
V 2.4.10 '<'   2.4.9    && P + || P _; EXPECT _; CODE
V 2.4.10 '<'   2.5.9    && P + || P _; EXPECT +; CODE
V 3.4.10 '<'   2.5.9    && P + || P _; EXPECT _; CODE
V 2.4.8  '>'   2.4.10   && P + || P _; EXPECT _; CODE
V 2.5.6  '<='  2.5.6    && P + || P _; EXPECT +; CODE
V 2.5.6  '>='  2.5.6    && P + || P _; EXPECT +; CODE
V 3.0    '<'   3.0.3    && P + || P _; EXPECT +; CODE
V 3.0002 '<'   3.0003.3 && P + || P _; EXPECT +; CODE
V 3.0002 '>'   3.0003.3 && P + || P _; EXPECT _; CODE
V 3.0003.3 '<' 3.0002   && P + || P _; EXPECT _; CODE
V 3.0003.3 '>' 3.0002   && P + || P _; EXPECT +; CODE

V 4.0-RC2 '>' 4.0-RC1   && P + || P _; EXPECT +; CODE
V 4.0-RC2 '<' 4.0-RC1   && P + || P _; EXPECT _; CODE

12
你可以使用递归在 . 上进行拆分,并按照以下算法进行比较,该算法取自于这里。如果版本相同,则返回10;如果版本1大于版本2,则返回11;否则返回9。
#!/bin/bash
do_version_check() {

   [ "$1" == "$2" ] && return 10

   ver1front=`echo $1 | cut -d "." -f -1`
   ver1back=`echo $1 | cut -d "." -f 2-`

   ver2front=`echo $2 | cut -d "." -f -1`
   ver2back=`echo $2 | cut -d "." -f 2-`

   if [ "$ver1front" != "$1" ] || [ "$ver2front" != "$2" ]; then
       [ "$ver1front" -gt "$ver2front" ] && return 11
       [ "$ver1front" -lt "$ver2front" ] && return 9

       [ "$ver1front" == "$1" ] || [ -z "$ver1back" ] && ver1back=0
       [ "$ver2front" == "$2" ] || [ -z "$ver2back" ] && ver2back=0
       do_version_check "$ver1back" "$ver2back"
       return $?
   else
           [ "$1" -gt "$2" ] && return 11 || return 9
   fi
}    

do_version_check "$1" "$2"

源代码


8

如果只是想知道一个版本是否比另一个低,可以使用命令 sort --version-sort 来检查我的版本字符串的顺序是否更改:

    string="$1
$2"
    [ "$string" == "$(sort --version-sort <<< "$string")" ]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接