How to increment version number in a shell script?

佐手、 提交于 2019-11-27 00:54:38

问题


The following simple version control script is meant to find the last version number of a given file, increment it, run a given command with the newly created file (e.g., editor), and after that save it to stable. Since it's simple, it doesn't check anything since the script would be modified as needed. For instance, if the result won't be stable, the user can omit the last argument.

However, one major concern of the current functionality is how to implement the following: if the last section after dot has two digits, inc untill 99; if only 1, then inc until 9, then move to the previous section. The versions may have any positive integer number of sections.

1.2.3.44 -> 1.2.3.45
1.2.3.9 -> 1.2.4.0
1.2.3 -> 1.2.4
9 -> 10

The remaining issue is that it doesn't wait for a tabbed wine editor to close the file; the goal is to detect when the tab is closed. Also, could you explain how best to make sure that my variable names don't overwrite existing ones?

You can also offer other improvements.

#!/bin/bash
#Tested on bash 4.1.5
#All arguments in order: "folder with file" "file pattern" cmd [stable name]
folder="$1"
file_pattern="$2"
cmd="$3"
stable="$4"

cd "$folder"
last_version=$(ls --format=single-column --almost-all | \
    grep "$file_pattern" | \
    sed -nr 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' | \
    sort -Vu | \
    tail -n 1)
last_version_file=$(ls --format=single-column --almost-all | \
    grep "$file_pattern" | \
    grep $last_version | \
    tail -n 1) #tail -n 1 is only needed to get 1 line if there are backup files with the same version number
new_version=$(echo $last_version | \
    gawk -F"." '{$NF+=1}{print $0RT}' OFS="." ORS="") #increments last section indefinitely
new_version_file=$(echo "$last_version_file" | \
    sed -r "s/$last_version/$new_version/")
cp "$last_version_file" "$new_version_file"
"$cmd" "$new_version_file" & \
    wait #works with gedit but not with wine tabbed editor
[[ "$stable" ]] && \
    cp "$new_version_file" "$stable" #True if the length of string is non-zero.

Update: The following works on my pc, I will update it if improvements or solutions to unsolved problems are found:

#!/bin/bash
inc()
{
shopt -s extglob
    num=${last_version//./}
    let num++

    re=${last_version//./)(}
    re=${re//[0-9]/.}')'
    re=${re#*)}

    count=${last_version//[0-9]/}
    count=$(wc -c<<<$count)
    out=''
    for ((i=count-1;i>0;i--)) ; do
        out='.\\'$i$out
    done

    sed -r s/$re$/$out/ <<<$num
}

folder="$1"
file_pattern="$2"
cmd="$3"
stable="$4"

cd "$folder"
last_version=$(ls --format=single-column --almost-all | \
    grep "$file_pattern" | \
    sed -nr 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p' | \
    sort -Vu | \
    tail -n 1) #--almost-all do not list implied . and ..
last_version_file=$(ls --format=single-column --almost-all | \
    grep "$file_pattern" | \
    grep $last_version | \
    tail -n 1) #tail -n 1 is only needed to get 1 line if there are backup files with the same version number
new_version=$(inc)
new_version_file=$(echo "$last_version_file" | \
    sed -r "s/$last_version/$new_version/")
cp "$last_version_file" "$new_version_file"
"$cmd" "$new_version_file" && \
    wait #works with gedit but not tabbed wine editor
[[ "$stable" ]] && \
    cp "$new_version_file" "$stable" #True if the length of string is non-zero.

I appreciate the variety of solutions that have been offered, for they help with gaining a perspective and drawing a comparison.


回答1:


$ echo 1.2.3.4 | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}'
1.2.3.5

1.2.3.9  => 1.2.4.0
1.2.3.44 => 1.2.3.45
1.2.3.99 => 1.2.4.00
1.2.3.999=> 1.2.4.000
1.2.9    => 1.3.0
999      => 1000

UPDATE:

#!/usr/bin/gawk -f

BEGIN{
    v[1] = "1.2.3.4"
    v[2] = "1.2.3.44"
    v[3] = "1.2.3.99"
    v[4] = "1.2.3"
    v[5] = "9"
    v[6] = "9.9.9.9"
    v[7] = "99.99.99.99"
    v[8] = "99.0.99.99"
    v[9] = ""

    for(i in v)
        printf("#%d: %s => %s\n", i, v[i], inc(v[i])) | "sort | column -t"
}

function inc(s,    a, len1, len2, len3, head, tail)
{
    split(s, a, ".")

    len1 = length(a)
    if(len1==0)
        return -1
    else if(len1==1)
        return s+1

    len2 = length(a[len1])
    len3 = length(a[len1]+1)

    head = join(a, 1, len1-1)
    tail = sprintf("%0*d", len2, (a[len1]+1)%(10^len2))

    if(len2==len3)
        return head "." tail
    else
        return inc(head) "." tail
}

function join(a, x, y,    s)
{
    for(i=x; i<y; i++)
        s = s a[i] "."
    return s a[y]
}

$ chmod +x inc.awk
$ ./inc.awk
#1:  1.2.3.4      =>  1.2.3.5
#2:  1.2.3.44     =>  1.2.3.45
#3:  1.2.3.99     =>  1.2.4.00
#4:  1.2.3        =>  1.2.4
#5:  9            =>  10
#6:  9.9.9.9      =>  10.0.0.0
#7:  99.99.99.99  =>  100.00.00.00
#8:  99.0.99.99   =>  99.1.00.00
#9:  =>           -1



回答2:


Here are a couple more flexible options. Both accept a second argument to indicate which position to increment.

1. Simple function

For more predictable input.

# Usage: increment_version <version> [<position>]
increment_version() {
 local v=$1
 if [ -z $2 ]; then 
    local rgx='^((?:[0-9]+\.)*)([0-9]+)($)'
 else 
    local rgx='^((?:[0-9]+\.){'$(($2-1))'})([0-9]+)(\.|$)'
    for (( p=`grep -o "\."<<<".$v"|wc -l`; p<$2; p++)); do 
       v+=.0; done; fi
 val=`echo -e "$v" | perl -pe 's/^.*'$rgx'.*$/$2/'`
 echo "$v" | perl -pe s/$rgx.*$'/${1}'`printf %0${#val}s $(($val+1))`/
}

# EXAMPLE   ------------->   # RESULT
increment_version 1          # 2
increment_version 1.0.0      # 1.0.1
increment_version 1 2        # 1.1
increment_version 1.1.1 2    # 1.2
increment_version 00.00.001  # 00.00.002

2. Robust function

For use with scripts, or more customizability to apply to various versioning systems. It could use a couple more options, but as it stands now it works for my projects using the "major.minor[.maintenance[.build]]" version sequences.

# Accepts a version string and prints it incremented by one.
# Usage: increment_version <version> [<position>] [<leftmost>]
increment_version() {
   local usage=" USAGE: $FUNCNAME [-l] [-t] <version> [<position>] [<leftmost>]
           -l : remove leading zeros
           -t : drop trailing zeros
    <version> : The version string.
   <position> : Optional. The position (starting with one) of the number 
                within <version> to increment.  If the position does not 
                exist, it will be created.  Defaults to last position.
   <leftmost> : The leftmost position that can be incremented.  If does not
                exist, position will be created.  This right-padding will
                occur even to right of <position>, unless passed the -t flag."

   # Get flags.
   local flag_remove_leading_zeros=0
   local flag_drop_trailing_zeros=0
   while [ "${1:0:1}" == "-" ]; do
      if [ "$1" == "--" ]; then shift; break
      elif [ "$1" == "-l" ]; then flag_remove_leading_zeros=1
      elif [ "$1" == "-t" ]; then flag_drop_trailing_zeros=1
      else echo -e "Invalid flag: ${1}\n$usage"; return 1; fi
      shift; done

   # Get arguments.
   if [ ${#@} -lt 1 ]; then echo "$usage"; return 1; fi
   local v="${1}"             # version string
   local targetPos=${2-last}  # target position
   local minPos=${3-${2-0}}   # minimum position

   # Split version string into array using its periods. 
   local IFSbak; IFSbak=IFS; IFS='.' # IFS restored at end of func to                     
   read -ra v <<< "$v"               #  avoid breaking other scripts.

   # Determine target position.
   if [ "${targetPos}" == "last" ]; then 
      if [ "${minPos}" == "last" ]; then minPos=0; fi
      targetPos=$((${#v[@]}>${minPos}?${#v[@]}:$minPos)); fi
   if [[ ! ${targetPos} -gt 0 ]]; then
      echo -e "Invalid position: '$targetPos'\n$usage"; return 1; fi
   (( targetPos--  )) || true # offset to match array index

   # Make sure minPosition exists.
   while [ ${#v[@]} -lt ${minPos} ]; do v+=("0"); done;

   # Increment target position.
   v[$targetPos]=`printf %0${#v[$targetPos]}d $((10#${v[$targetPos]}+1))`;

   # Remove leading zeros, if -l flag passed.
   if [ $flag_remove_leading_zeros == 1 ]; then
      for (( pos=0; $pos<${#v[@]}; pos++ )); do
         v[$pos]=$((${v[$pos]}*1)); done; fi

   # If targetPosition was not at end of array, reset following positions to
   #   zero (or remove them if -t flag was passed).
   if [[ ${flag_drop_trailing_zeros} -eq "1" ]]; then
        for (( p=$((${#v[@]}-1)); $p>$targetPos; p-- )); do unset v[$p]; done
   else for (( p=$((${#v[@]}-1)); $p>$targetPos; p-- )); do v[$p]=0; done; fi

   echo "${v[*]}"
   IFS=IFSbak
   return 0
}

# EXAMPLE   ------------->   # RESULT
increment_version 1          # 2
increment_version 1 2        # 1.1
increment_version 1 3        # 1.0.1
increment_version 1.0.0      # 1.0.1
increment_version 1.2.3.9    # 1.2.3.10
increment_version 00.00.001  # 00.00.002
increment_version -l 00.001  # 0.2
increment_version 1.1.1.1 2   # 1.2.0.0
increment_version -t 1.1.1 2  # 1.2
increment_version v1.1.3      # v1.1.4
increment_version 1.2.9 2 4     # 1.3.0.0
increment_version -t 1.2.9 2 4  # 1.3
increment_version 1.2.9 last 4  # 1.2.9.1

Obviously, this is excessive just to increment a version string. But I wrote this because I had a need for different types of projects, and because if speed is not an issue, I prefer reusability over tweaking the same code across dozens of scripts. I guess that's just my object-oriented side leaking into my scripts.




回答3:


Here is an even shorter version that also supports a postfix (nice for -SNAPSHOT)

$ cat versions
1.2.3.44
1.2.3.9
1.2.3
9
42.2-includes-postfix

$ perl -pe 's/^((\d+\.)*)(\d+)(.*)$/$1.($3+1).$4/e' < versions
1.2.3.45
1.2.3.10
1.2.4
10
42.3-includes-postfix

Explanation

I used regex to capture 3 parts. The stuff before the last position, the number to increment, and the stuff after.

  • ((\d+\.)*) - stuff of the from 1.1.1.1.1.
  • (\d+) - the last digit
  • (.*) - the stuff after the last digit

I then use the e option to allow expressions in the replace part. Note with the e option \1 becomes a variable $1 and you need to concatenate variables with the dot operator.

  • $1 - the capture group of 1.1.1.1.1.
  • ($3+1) - increment the last digit. note $2 is used in the sub group of $1 to get the repeated 1.
  • $4 - the stuff after the last digit



回答4:


Pure Bash:

increment_version ()
{
  declare -a part=( ${1//\./ } )
  declare    new
  declare -i carry=1

  for (( CNTR=${#part[@]}-1; CNTR>=0; CNTR-=1 )); do
    len=${#part[CNTR]}
    new=$((part[CNTR]+carry))
    [ ${#new} -gt $len ] && carry=1 || carry=0
    [ $CNTR -gt 0 ] && part[CNTR]=${new: -len} || part[CNTR]=${new}
  done
  new="${part[*]}"
  echo -e "${new// /.}"
} 

version='1.2.3.44'

increment_version $version

result:

1.2.3.45

The version string is split and stored in the array part. The loop goes from the last to the first part of the version. The last part will be incremented and possibly cut down to its original length. A carry is taken to the next part.




回答5:


Determining a version number for a software project is based on its relative change / functionality / development stage / revision. Consequent increments to the version and revision numbering is ideally a process that should be done by a human. However, not to second-guess your motivation for writing this script, here is my suggestion.

Include some logic in your script that will do exactly what you describe in your requirement

"...if the last section after dot has two digits, inc until 99; if only 1, then inc until 9 ... "

Assuming the third position is the development stage number $dNum and the fourth (last) position is the revision number $rNum:

if  [ $(expr length $rNum) = "2" ] ; then 
    if [ $rNum -lt 99 ]; then 
        rNum=$(($rNum + 1))
    else rNum=0
         dNum=$(($dNum + 1)) #some additional logic for $dNum > 9 also needed
    fi
elif [ $(expr length $dNum) = "1" ] ; then
    ...
    ...
fi

Perhaps a function will allow the most succinct way of handling all positions (majNum.minNum.dNum.rNum).

You will have to separate the project name and version number components of your filename in your script and then construct the version number with all its positions, and finally reconstruct the filename with something like

new_version="$majNum.minNum.$dNum.$rNum"
new_version_file="$old_file.$new_version"

Hope that helps and check this SO discussion as well as this wikipedia entry if you want to know more about versioning conventions.




回答6:


Tired of bash? Why not try Perl?

$ cat versions
1.2.3.44
1.2.3.9
1.2.3
9

$ cat versions | perl -ne 'chomp; print join(".", splice(@{[split/\./,$_]}, 0, -1), map {++$_} pop @{[split/\./,$_]}), "\n";'
1.2.3.45
1.2.3.10
1.2.4
10

Not quite in compliance with the requirement, of course.




回答7:


Using just bash, wc and sed:

#! /bin/bash
for v in 1.2.3.44 1.2.3.9 1.2.3 9 1.4.29.9 9.99.9 ; do
    echo -n $v '-> '

    num=${v//./}
    let num++

    re=${v//./)(}
    re=${re//[0-9]/.}')'
    re=${re#*)}

    count=${v//[0-9]/}
    count=$(wc -c<<<$count)
    out=''
    for ((i=count-1;i>0;i--)) ; do
        out='.\'$i$out
    done

    sed -r s/$re$/$out/ <<<$num
done



回答8:


Another option is to use Python. I think this way it a bit more readable than using plain Bash or Perl.

function increase_version() {
    python - "$1" <<EOF
import sys
version = sys.argv[1]
base, _, minor = version.rpartition('.')
print(base + '.' + str(int(minor) + 1))
EOF
}


来源:https://stackoverflow.com/questions/8653126/how-to-increment-version-number-in-a-shell-script

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!