How to increment version number in a shell script?
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.
$ 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
For just incrementing the dewey decimal version:
awk -F. -v OFS=. '{$NF += 1 ; print}'
Or in a shell script:
NEXTVERSION=$(echo ${VERSION} | awk -F. -v OFS=. '{$NF += 1 ; print}')
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
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.