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

前端 未结 29 1287
慢半拍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 06:57

    Here's another pure bash version, rather smaller than the accepted answer. It only checks whether a version is less than or equal to a "minimum version", and it will check alphanumeric sequences lexicographically, which often gives the wrong result ("snapshot" is not later than "release", to give a common example). It will work fine for major/minor.

    is_number() {
        case "$BASH_VERSION" in
            3.1.*)
                PATTERN='\^\[0-9\]+\$'
                ;;
            *)
                PATTERN='^[0-9]+$'
                ;;
        esac
    
        [[ "$1" =~ $PATTERN ]]
    }
    
    min_version() {
        if [[ $# != 2 ]]
        then
            echo "Usage: min_version current minimum"
            return
        fi
    
        A="${1%%.*}"
        B="${2%%.*}"
    
        if [[ "$A" != "$1" && "$B" != "$2" && "$A" == "$B" ]]
        then
            min_version "${1#*.}" "${2#*.}"
        else
            if is_number "$A" && is_number "$B"
            then
                [[ "$A" -ge "$B" ]]
            else
                [[ ! "$A" < "$B" ]]
            fi
        fi
    }
    
    0 讨论(0)
  • 2020-11-22 06:59

    I'm using embedded Linux (Yocto) with BusyBox. BusyBox sort doesn't have a -V option (but BusyBox expr match can do regular expressions). So I needed a Bash version compare which worked with that constraint.

    I've made the following (similar to Dennis Williamson's answer) to compare using a "natural sort" type of algorithm. It splits the string into numeric parts and non-numeric parts; it compares the numeric parts numerically (so 10 is greater than 9), and compares the non-numeric parts as a plain ASCII comparison.

    ascii_frag() {
        expr match "$1" "\([^[:digit:]]*\)"
    }
    
    ascii_remainder() {
        expr match "$1" "[^[:digit:]]*\(.*\)"
    }
    
    numeric_frag() {
        expr match "$1" "\([[:digit:]]*\)"
    }
    
    numeric_remainder() {
        expr match "$1" "[[:digit:]]*\(.*\)"
    }
    
    vercomp_debug() {
        OUT="$1"
        #echo "${OUT}"
    }
    
    # return 1 for $1 > $2
    # return 2 for $1 < $2
    # return 0 for equal
    vercomp() {
        local WORK1="$1"
        local WORK2="$2"
        local NUM1="", NUM2="", ASCII1="", ASCII2=""
        while true; do
            vercomp_debug "ASCII compare"
            ASCII1=`ascii_frag "${WORK1}"`
            ASCII2=`ascii_frag "${WORK2}"`
            WORK1=`ascii_remainder "${WORK1}"`
            WORK2=`ascii_remainder "${WORK2}"`
            vercomp_debug "\"${ASCII1}\" remainder \"${WORK1}\""
            vercomp_debug "\"${ASCII2}\" remainder \"${WORK2}\""
    
            if [ "${ASCII1}" \> "${ASCII2}" ]; then
                vercomp_debug "ascii ${ASCII1} > ${ASCII2}"
                return 1
            elif [ "${ASCII1}" \< "${ASCII2}" ]; then
                vercomp_debug "ascii ${ASCII1} < ${ASCII2}"
                return 2
            fi
            vercomp_debug "--------"
    
            vercomp_debug "Numeric compare"
            NUM1=`numeric_frag "${WORK1}"`
            NUM2=`numeric_frag "${WORK2}"`
            WORK1=`numeric_remainder "${WORK1}"`
            WORK2=`numeric_remainder "${WORK2}"`
            vercomp_debug "\"${NUM1}\" remainder \"${WORK1}\""
            vercomp_debug "\"${NUM2}\" remainder \"${WORK2}\""
    
            if [ -z "${NUM1}" -a -z "${NUM2}" ]; then
                vercomp_debug "blank 1 and blank 2 equal"
                return 0
            elif [ -z "${NUM1}" -a -n "${NUM2}" ]; then
                vercomp_debug "blank 1 less than non-blank 2"
                return 2
            elif [ -n "${NUM1}" -a -z "${NUM2}" ]; then
                vercomp_debug "non-blank 1 greater than blank 2"
                return 1
            fi
    
            if [ "${NUM1}" -gt "${NUM2}" ]; then
                vercomp_debug "num ${NUM1} > ${NUM2}"
                return 1
            elif [ "${NUM1}" -lt "${NUM2}" ]; then
                vercomp_debug "num ${NUM1} < ${NUM2}"
                return 2
            fi
            vercomp_debug "--------"
        done
    }
    

    It can compare more complicated version numbers such as

    • 1.2-r3 versus 1.2-r4
    • 1.2rc3 versus 1.2r4

    Note that it doesn't return the same result for some of the corner-cases in Dennis Williamson's answer. In particular:

    1            1.0          <
    1.0          1            >
    1.0.2.0      1.0.2        >
    1..0         1.0          >
    1.0          1..0         <
    

    But those are corner cases, and I think the results are still reasonable.

    0 讨论(0)
  • 2020-11-22 06:59

    Wow... this is way down the list of an old question, but I think this is a pretty elegant answer. First convert each dot-separated version into its own array, using shell parameter expansion (See Shell Parameter Expansion).

    v1="05.2.3"     # some evil examples that work here
    v2="7.001.0.0"
    
    declare -a v1_array=(${v1//./ })
    declare -a v2_array=(${v2//./ })
    

    Now the two arrays have the version number as a numerical string in priority order. Lots of the above solutions take you from there, but it all derives from the observation that version string is just an integer with an arbitrary base. We can test finding the first unequal digit (like strcmp does for characters in a string).

    compare_version() {
      declare -a v1_array=(${1//./ })
      declare -a v2_array=(${2//./ })
    
      while [[ -nz $v1_array ]] || [[ -nz $v2_array ]]; do
        let v1_val=${v1_array:-0}  # this will remove any leading zeros
        let v2_val=${v2_array:-0}
        let result=$((v1_val-v2_val))
    
        if (( result != 0 )); then
          echo $result
          return
        fi
    
        v1_array=("${v1_array[@]:1}") # trim off the first "digit". it doesn't help
        v2_array=("${v2_array[@]:1}")
      done
    
      # if we get here, both the arrays are empty and neither has been numerically
      # different, which is equivalent to the two versions being equal
    
      echo 0
      return
    }
    

    This echoes a negative number if the first version is less than the second, a zero if they're equal and a positive number if the first version is greater. Some output:

    $ compare_version 1 1.2
    -2
    $ compare_version "05.1.3" "5.001.03.0.0.0.1"
    -1
    $ compare_version "05.1.3" "5.001.03.0.0.0"
    0
    $ compare_version "05.1.3" "5.001.03.0"
    0
    $ compare_version "05.1.3" "5.001.30.0"
    -27
    $ compare_version "05.2.3" "7.001.0.0"
    -2
    $ compare_version "05.1.3" "5.001.30.0"
    -27
    $ compare_version "7.001.0.0" "05.1.3"
    2
    

    Degenerate cases like, ".2" or "3.0." don't work (undefined results), and if non-numeric characters are present beside the '.' it might fail (haven't tested) but will certainly be undefined. So this should be paired with a sanitizing function or appropriate check for valid formatting. Also, I'm sure with some tweaking, this could be made more robust without too much extra baggage.

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

    Thanks to Dennis's solution, we can extend it to allow comparison operators '>', '<', '=', '==', '<=', and '>='.

    # compver ver1 '=|==|>|<|>=|<=' ver2
    compver() { 
        local op
        vercomp $1 $3
        case $? in
            0) op='=';;
            1) op='>';;
            2) op='<';;
        esac
        [[ $2 == *$op* ]] && return 0 || return 1
    }
    

    We can then use comparison operators in the expressions like:

    compver 1.7 '<=' 1.8
    compver 1.7 '==' 1.7
    compver 1.7 '=' 1.7
    

    and test only the true/false of the result, like:

    if compver $ver1 '>' $ver2; then
        echo "Newer"
    fi
    
    0 讨论(0)
  • 2020-11-22 07:02
    function version { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; }
    

    Used as such:

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

    (from https://apple.stackexchange.com/a/123408/11374)

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

    You can use version CLI to check version's constraints

    $ version ">=1.0, <2.0" "1.7"
    $ go version | version ">=1.9"
    

    Bash script example:

    #!/bin/bash
    
    if `version -b ">=9.0.0" "$(gcc --version)"`; then
      echo "gcc version satisfies constraints >=9.0.0"
    else
      echo "gcc version doesn't satisfies constraints >=9.0.0"
    fi
    
    0 讨论(0)
提交回复
热议问题