46

I'm trying to write a bash script that increments the version number which is given in

{major}.{minor}.{revision}

For example.

1.2.13

Is there a good way to easily extract those 3 numbers using something like sed or awk such that I could increment the {revision} number and output the full version number string.

Dougnukem
  • 14,709
  • 24
  • 89
  • 130

9 Answers9

76
$ v=1.2.13
$ echo "${v%.*}.$((${v##*.}+1))"
1.2.14

$ v=11.1.2.3.0
$ echo "${v%.*}.$((${v##*.}+1))"
11.1.2.3.1

Here is how it works:

The string is split in two parts.

  • the first one contains everything but the last dot and next characters: ${v%.*}
  • the second one contains everything but all characters up to the last dot: ${v##*.}

The first part is printed as is, followed by a plain dot and the last part incremented using shell arithmetic expansion: $((x+1))

jlliagre
  • 29,783
  • 6
  • 61
  • 72
52

Pure Bash using an array:

version='1.2.33'
a=( ${version//./ } )                   # replace points, split into array
((a[2]++))                              # increment revision (or other part)
version="${a[0]}.${a[1]}.${a[2]}"       # compose new version
Fritz G. Mehner
  • 16,550
  • 2
  • 34
  • 41
  • 2
    I like this version as it makes no assumption about how many parts the version has. To compose the new version one would have to set `IFS` and use `"${a[*]}"`, though. Thanks, was able to put it to good use! – Florian Jan 25 '13 at 12:55
49

I prefer "cut" command for this kind of things

major=`echo $version | cut -d. -f1`
minor=`echo $version | cut -d. -f2`
revision=`echo $version | cut -d. -f3`
revision=`expr $revision + 1`

echo "$major.$minor.$revision"

I know this is not the shortest way, but for me it's simplest to understand and to read...

Eedoh
  • 5,818
  • 9
  • 38
  • 62
  • 2
    4 subshells, 3 pipes, 3 complete processes (at least) just to change one or two characters! That is a little too much. – Fritz G. Mehner Jun 06 '11 at 16:38
  • 6
    @fgm but at least it works whether you're using bash or not. +1 And you can of course simplify this: echo "$(echo $version | cut -d. -f-2).$(expr $(echo $version | cut -d. -f3) + 1)" it is still a lot of subshells and pipes, but unless you're using a super micro under-powered embedded device that should not matter. If you are after speed, then bash is perhaps not the best choice in the first place ;-) – Huygens Sep 09 '17 at 09:29
14

Yet another shell way (showing there's always more than one way to bugger around with this stuff...):

$ echo 1.2.3 | ( IFS=".$IFS" ; read a b c && echo $a.$b.$((c + 1)) )
1.2.4

So, we can do:

$ x=1.2.3
$ y=`echo $x | ( IFS=".$IFS" ; read a b c && echo $a.$b.$((c + 1)) )`
$ echo $y
1.2.4
Chris J
  • 30,688
  • 6
  • 69
  • 111
  • +1, very readable way to skin a cat. Can you explain why it was necessary to write IFS=".$IFS" instead of simply IFS="." ? I've checked IFS="." and it does not work correctly. – bbaja42 Jun 05 '11 at 21:40
  • Paranoia on my part, as *something* will fail to work if the default's not there as well :-) My guess is that if IFS doesn't include the newline (by default it's got a space, a tab and a newline), then read doesn't work properly. That said, I've just tried it here, and doing IFS="." works fine here... (bash 3.2.51 running under cygwin). – Chris J Jun 05 '11 at 21:51
9

Awk makes it quite simple:

echo "1.2.14" | awk -F \. {'print $1,$2, $3'} will print out 1 2 14.

flag -F specifies separator.

If you wish to save one of the values:

firstVariable=$(echo "1.2.14" | awk -F \. {'print $1'})

bbaja42
  • 2,099
  • 18
  • 34
7

I use the shell's own word splitting; something like

oIFS="$IFS"
IFS=.
set -- $version
IFS="$oIFS"

although you need to be careful with version numbers in general due to alphabetic or date suffixes and other annoyingly inconsistent bits. After this, the positional parameters will be set to the components of $version:

$1 = 1
$2 = 2
$3 = 13

($IFS is a set of single characters, not a string, so this won't work with a multicharacter field separator, although you can use IFS=.- to split on either . or -.)

geekosaur
  • 59,309
  • 11
  • 123
  • 114
  • I'm not sure this has anything to do with the problem I asked, maybe I'm misunderstanding what $IFS is and what $version is being set to. – Dougnukem Jun 05 '11 at 19:54
  • `$IFS` is what the shell uses to do its own field ("word") splitting; I am saving the original value (space, tab, newline) and setting it to `.`, then using `set` to force `$version` to be word split. I'll expand on the answer. – geekosaur Jun 05 '11 at 20:02
  • mod +1: Very ingenious. IFS is the "Input Field Separator" used by the shell. It is normally set to tab, space, return. geekasaur is changing it to the period. The `set -- $version` is replacing the command line parameters with the $version field which is split by periods. Thus, the three portions are now `$1`, `$2`, `$3`. If you don't want to use set, you can instead try this: `echo $version | read major minor revision`. It's not as compact, but doesn't mess with your command line parameters which you still might be using. – David W. Jun 05 '11 at 21:32
  • @David: note that the `while` loop may run in a subshell, with possibly surprising results (for example, variable settings in the loop won't be seen byt he rest of the script). http://stackoverflow.com/questions/6245246/translating-bin-ksh-date-conversion-function-into-bin-sh – geekosaur Jun 05 '11 at 21:35
  • Man you're fast. I saw the `while` line in my comments and removed it. It didn't take me more than a minute. Old habit of piping input into a while read loop. Actually, what I put doesn't work in BASH, but does work in Kornshell which is what I use. It's fully compatible with BASH, except when it isn't. In order for this to work in bash, you have to put `$version` as a here document. – David W. Jun 05 '11 at 21:58
  • What geekosaur wrote is exactly what the doctor ordered! I tested David W.'s idea of using `echo $version | read major minor revision` instead of `set -- $version` but that didn't work at all. – smithfarm Aug 22 '12 at 21:50
2

Inspired by the answer of jlliagre I made my own version which supports version numbers just having a major version given. jlliagre's version will make 1 -> 1.2 instead of 2.

This one is appropriate to both styles of version numbers:

function increment_version()
    local VERSION="$1"

    local INCREMENTED_VERSION=
    if [[ "$VERSION" =~ .*\..* ]]; then
        INCREMENTED_VERSION="${VERSION%.*}.$((${VERSION##*.}+1))"
    else
        INCREMENTED_VERSION="$((${VERSION##*.}+1))"
    fi

    echo "$INCREMENTED_VERSION"
}

This will produce the following outputs:

increment_version 1         -> 2 
increment_version 1.2       -> 1.3    
increment_version 1.2.9     -> 1.2.10 
increment_version 1.2.9.101 -> 1.2.9.102
Community
  • 1
  • 1
Sven Driemecker
  • 3,421
  • 1
  • 20
  • 22
0

Small variation on fgm's solution using the builtin read command to split the string into an array. Note that the scope of the IFS variable is limited to the read command (so no need to store & restore the current IFS variable).

version='1.2.33'
IFS='.' read -r -a a <<<"$version"
((a[2]++))
printf '%s\n' "${a[@]}" | nl
version="${a[0]}.${a[1]}.${a[2]}"
echo "$version"

See: How do I split a string on a delimiter in Bash?

Community
  • 1
  • 1
tylo
  • 1
0

I'm surprised no one suggested grep yet.

Here's how to get the full version (not limited to the length of x.y.z...) from a file name:

filename="openshift-install-linux-4.12.0-ec.3.tar.gz"
find -name "$filename" | grep -Eo '([0-9]+)(\.?[0-9]+)*' | head -1
# 4.12.0
Noam Manos
  • 15,216
  • 3
  • 86
  • 85