How to compare two strings in dot separated version format in Bash?

前端 未结 29 1341
慢半拍i
慢半拍i 2020-11-22 06:52

Is there any way to compare such strings on bash, e.g.: 2.4.5 and 2.8 and 2.4.5.1?

相关标签:
29条回答
  • 2020-11-22 07:20

    I implemented yet another comparator function. This one had two specific requirements: (i) I didn't want the function to fail by using return 1 but echo instead; (ii) as we're retrieving versions from a git repository version "1.0" should be bigger than "1.0.2", meaning that "1.0" comes from trunk.

    function version_compare {
      IFS="." read -a v_a <<< "$1"
      IFS="." read -a v_b <<< "$2"
    
      while [[ -n "$v_a" || -n "$v_b" ]]; do
        [[ -z "$v_a" || "$v_a" -gt "$v_b" ]] && echo 1 && return
        [[ -z "$v_b" || "$v_b" -gt "$v_a" ]] && echo -1 && return
    
        v_a=("${v_a[@]:1}")
        v_b=("${v_b[@]:1}")
      done
    
      echo 0
    }
    

    Feel free to comment and suggest improvements.

    0 讨论(0)
  • 2020-11-22 07:21

    If you have coreutils-7 (in Ubuntu Karmic but not Jaunty) then your sort command should have a -V option (version sort) which you could use to do the comparison:

    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
    
    0 讨论(0)
  • 2020-11-22 07:21

    Here's a pure Bash solution that supports revisions (e.g. '1.0-r1'), based on the answer posted by Dennis Williamson. It can easily be modified to support stuff like '-RC1' or extract the version from a more complex string by changing the regular expression.

    For details regarding the implementation, please refer to in-code comments and/or enable the included debug code:

    #!/bin/bash
    
    # Compare two version strings [$1: version string 1 (v1), $2: version string 2 (v2)]
    # Return values:
    #   0: v1 == v2
    #   1: v1 > v2
    #   2: v1 < v2
    # Based on: https://stackoverflow.com/a/4025065 by Dennis Williamson
    function compare_versions() {
    
        # Trivial v1 == v2 test based on string comparison
        [[ "$1" == "$2" ]] && return 0
    
        # Local variables
        local regex="^(.*)-r([0-9]*)$" va1=() vr1=0 va2=() vr2=0 len i IFS="."
    
        # Split version strings into arrays, extract trailing revisions
        if [[ "$1" =~ ${regex} ]]; then
            va1=(${BASH_REMATCH[1]})
            [[ -n "${BASH_REMATCH[2]}" ]] && vr1=${BASH_REMATCH[2]}
        else
            va1=($1)
        fi
        if [[ "$2" =~ ${regex} ]]; then
            va2=(${BASH_REMATCH[1]})
            [[ -n "${BASH_REMATCH[2]}" ]] && vr2=${BASH_REMATCH[2]}
        else
            va2=($2)
        fi
    
        # Bring va1 and va2 to same length by filling empty fields with zeros
        (( ${#va1[@]} > ${#va2[@]} )) && len=${#va1[@]} || len=${#va2[@]}
        for ((i=0; i < len; ++i)); do
            [[ -z "${va1[i]}" ]] && va1[i]="0"
            [[ -z "${va2[i]}" ]] && va2[i]="0"
        done
    
        # Append revisions, increment length
        va1+=($vr1)
        va2+=($vr2)
        len=$((len+1))
    
        # *** DEBUG ***
        #echo "TEST: '${va1[@]} (?) ${va2[@]}'"
    
        # Compare version elements, check if v1 > v2 or v1 < v2
        for ((i=0; i < len; ++i)); do
            if (( 10#${va1[i]} > 10#${va2[i]} )); then
                return 1
            elif (( 10#${va1[i]} < 10#${va2[i]} )); then
                return 2
            fi
        done
    
        # All elements are equal, thus v1 == v2
        return 0
    }
    
    # Test compare_versions [$1: version string 1, $2: version string 2, $3: expected result]
    function test_compare_versions() {
        local op
        compare_versions "$1" "$2"
        case $? in
            0) op="==" ;;
            1) op=">" ;;
            2) op="<" ;;
        esac
        if [[ "$op" == "$3" ]]; then
            echo -e "\e[1;32mPASS: '$1 $op $2'\e[0m"
        else
            echo -e "\e[1;31mFAIL: '$1 $3 $2' (result: '$1 $op $2')\e[0m"
        fi
    }
    
    echo -e "\nThe following tests should pass:"
    while read -r test; do
        test_compare_versions $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         ==
    1.0-r1       1.0-r3       <
    1.0-r9       2.0          <
    3.0-r15      3.0-r9       >
    ...-r1       ...-r2       <
    2.0-r1       1.9.8.21-r2  >
    1.0          3.8.9.32-r   <
    -r           -r3          <
    -r3          -r           >
    -r3          -r3          ==
    -r           -r           ==
    0.0-r2       0.0.0.0-r2   ==
    1.0.0.0-r2   1.0-r2       ==
    0.0.0.1-r7   -r9          >
    0.0-r0       0            ==
    1.002.0-r6   1.2.0-r7     <
    001.001-r2   1.1-r2       ==
    5.6.1-r0     5.6.1        ==
    EOF
    
    echo -e "\nThe following tests should fail:"
    while read -r test; do
        test_compare_versions $test
    done << EOF
    1            1            >
    3.0.5-r5     3..5-r5      >
    4.9.21-r3    4.8.22-r9    <
    1.0-r        1.0-r1       ==
    -r           1.0-r        >
    -r1          0.0-r1       <
    -r2          0-r2         <
    EOF
    
    echo -e "\nThe following line should be empty (local variables test):"
    echo "$op $regex $va1 $vr1 $va2 $vr2 $len $i $IFS"
    
    0 讨论(0)
  • 2020-11-22 07:22
    • Function V - pure bash solution, no external utilities required.
    • Supports = == != < <= > and >= (lexicographic).
    • Optional tail letter comparison: 1.5a < 1.5b
    • Unequal length comparison: 1.6 > 1.5b
    • Reads left-to-right: 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
    }
    

    Code Explained

    Line 1: Define local variables:

    • a, op, b - comparison operands and operator, i.e., "3.6" > "3.5a".
    • al, bl - letter tails of a and b, initialized to the tail item, i.e., "6" and "5a".

    Lines 2, 3: Left-trim digits from the tail items so only letters are left, if any, i.e., "" and "a".

    Line 4: Right trim letters from a and b to leave just the sequence of numeric items as local variables ai and bi, i.e., "3.6" and "3.5". Notable example: "4.01-RC2" > "4.01-RC1" yields ai="4.01" al="-RC2" and bi="4.01" bl="-RC1".

    Line 6: Define local variables:

    • ap, bp - zero right-paddings for ai and bi. Start by keeping the inter-item dots only, of which number equals the number of elements of a and b respectively.

    Line 7: Then append "0" after each dot to make padding masks.

    Line 9: Local variables:

    • w - item width
    • fmt - printf format string, to be calculated
    • x - temporary
    • With IFS=. bash splits variable values at '.'.

    Line 10: Calculate w, the maximum item width, which will be used to align items for lexicographic comparison. In our example w=2.

    Line 11: Create the printf alignment format by replacing each character of $a.$b with %${w}s, i.e., "3.6" > "3.5a" yields "%2s%2s%2s%2s".

    Line 12: "printf -v a" sets the value of variable a. This is equivalent to a=sprintf(...) in many programming languages. Note that here, by effect of IFS=. the arguments to printf split into individual items.

    With the first printf items of a are left-padded with spaces while enough "0" items are appended from bp to ensure that the resulting string a can be meaningfully compared to a similarly formatted b.

    Note that we append bp - not ap to ai because ap and bp may have different lenghts, so this results in a and b having equal lengths.

    With the second printf we append the letter part al to a with enough padding to enable meaningful comparison. Now a is ready for comparison with b.

    Line 13: Same as line 12 but for b.

    Line 15: Split comparison cases between non-built-in (<= and >=) and built-in operators.

    Line 16: If the comparison operator is <= then test for a<b or a=b - respectively >= a<b or a=b

    Line 17: Test for built-in comparison operators.

    <>

    # 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
    
    0 讨论(0)
  • 2020-11-22 07:22
    ### the answer is does we second argument is higher
    function _ver_higher {
            ver=`echo -ne "$1\n$2" |sort -Vr |head -n1`
            if [ "$2" == "$1" ]; then
                    return 1
            elif [ "$2" == "$ver" ]; then
                    return 0
            else
                    return 1
            fi
    }
    
    if _ver_higher $1 $2; then
            echo higher
    else
            echo same or less
    fi
    

    It's pretty simple and small.

    0 讨论(0)
提交回复
热议问题