This self-answered question addresses the following scenario:
A version number embedded in a text file is to be incremented.
Sample text-file content:
nuspec{
id = XXX;
version: 0.0.30;
title: XXX;
For instance, I want embedded version number 0.0.30
updated to 0.0.31
.
The line of interest can be assumed to match the following regex: ^\s+version: (.+);$
Note hat the intent is not to replace the version number with a fixed new version, but to increment the existing version.
Ideally, the increment logic would handle version strings representing either [version]
(System.Version
) or [semver]
(System.Management.Automation.SemanticVersion
) instances, ranging from 2 - 4 components; e.g.:
1.0
1.0.2
1.0.2.3
- [version]
format (up to 4 numeric components)1.0.2-preview2
- [semver]
format (up to 3 numeric components), optionally with a -
-separated preview label1.0.2-preview2+001
- ditto, additionally with a +
-separated build labelIn PowerShell (Core) 7+, a concise solution for incrementing the last numeric component is possible:
# PowerShell v7+ only
$file = 'somefile.txt'
(Get-Content -Raw $file) -replace '(?m)(?<=^\s+version: ).+(?=;$)', {
# Increment the *last numeric* component of the version number.
# See below for how to target other components.
$_.Value -replace '(?<=\.)\d+(?=$|-)', { 1 + $_.Value }
} | Set-Content $file
Note:
-Encoding
with Set-Content
if you need a different encoding.-Raw
, the command reads the entire file into memory first, which enables writing back to that same file in the same pipeline; however, there is a slight risk of data loss if writing back to the input file gets interrupted.-replace
invariably replaces all substrings that match the regex.(?m)
ensures that ^
and $
match the start and end of individual lines, which is necessary due to Get-Content -Raw
reading the entire file as a single, multi-line string.
Note:
For simplicity, text-based manipulation of the version string is performed, but you could also cast $_.Value
to [version]
or [semver]
(PowerShell [Core] v6+ only) and work with that (see next section).
The advantage of the text-based manipulation is the concise ability to retain all other components of the input version string as-is, without adding previously unspecified ones.
The above relies on the -replace
operator's ability to perform regex-based string substitutions fully dynamically, via a script block ({ ... }
) - as explained in this answer.
The regexes use look-around assertions ((?<=...)
and (?=...)
) so as to ensure that only the part of the input to be modified is matched.
(?<=^\s+version: )
and (?=;$)
look-arounds are specific to the sample file format; adjust these parts as needed to match the version number in your file format.To properly increment any numeric component - which includes setting the lower version components to 0
- more work is needed:
The most robust way is to cast the version string to a [version]
or [semver]
instance, and work with these objects.
Assuming that you have defined the Increment-Version
function whose source code is in the bottom section, you can use the following, which increments the minor version-number component, for instance:
# PowerShell v7+ only
$file = 'somefile.txt'
(Get-Content -Raw $file) -replace '(?m)(?<=^\s+version: ).+(?=;$)', {
# For [semver], use 'Major', 'Minor', or 'Patch'.
# For [version], use 'Major', 'Minor', 'Build', or 'Revision'.
[semver] $_.Value | Increment-Version Minor
}
The Windows PowerShell equivalent is the following, necessitating the direct use of [regex]::Replace()
, because script-block-based -replace
operations aren't supported there (note that type [semver]
isn't available there, so [version]
is used):
$file = 'somefile.txt'
[regex]::Replace(
(Get-Content -Raw $file),
'(?m)(?<=^\s+version: ).+(?=;$)',
{
param($m)
# For [semver], use 'Major', 'Minor', or 'Patch'.
# For [version], use 'Major', 'Minor', 'Build', or 'Revision'.
[version] $m.Value | Increment-Version Minor
}
)
Increment-Version
source code:
function Increment-Version {
<#
.SYNOPSIS
Increments the specified component of a given version number.
.DESCRIPTION
Operates on either [semver] or [version] version-number objects and increments
the specified numeric component, setting all lower components to 0.
If you don't specify a target -Component, the smallest type-appropriate
numeric component is used: 'Patch' for [semver], 'Revision' for [version].
-Component Revision is specific to [version], -Component Patch and
-Component Build can be used interchangeably to refer to the 3rd component.
Simpy pipe to | ForEach-Object ToString to convert the output objects to
version *strings*.
Note:
* If you don't supply [semver] or [version] instances as input, they input
objects are converted as follows:
* If [semver] is available, an attempt to convert to [semver] is made first.
* If [semver] isn't available or conversion failed or 'Revision' is
specified as the target component, conversion to [version] is attempted.
* To avoid ambiguity, provide [semver] or [version] instances as input.
* The 'Revision' -Component value applies to [version] instances only.
The 'Patch' and 'Build' values can be used interchangeably to target the
3rd component of either type.
* For [semver], if a prerelease and/or build label is present, it is kept as-is.
* For [version], missing build and revision numbers are retained as such,
if possible, so that the stringified version of the output object retains
its format.
.NOTES
"Increment" is not an approved verb, but it is used nonetheless, because
none of the approved verbs can express the operation's intent succinctly.
.EXAMPLE
'1.0' | Increment-Version Minor
Outputs the equivalent of [semver] '1.1.0', if [semver] is available,
otherwise the equivalent of [version] '1.1'
.EXAMPLE
'1.0' | Increment-Version Revision
Outputs the equivalent of [version] '1.0.0.1'
.EXAMPLE
'1.0.2-releaseX+buildY' | Increment-Version Minor
Outputs the equivalent of [semver] '1.1.0-releaseX+buildY'
.EXAMPLE
[version ] '1.0' | Increment-Version | ForEach-Object ToString
Outputs '1.0.0.1', i.e. the equivalent of ([version] '1.0.0.1').ToString()
#>
param(
# Note: 'Patch' and 'Build' can be used interchangeably to refer to the 3rd component.
# 'Revision' applies to [version] only
[ValidateSet('Major', 'Minor', 'Patch', 'Build', 'Revision')] [string] $Component,
[Parameter(Mandatory, ValueFromPipeline)] [object] $Version
)
begin {
Set-StrictMode -Version 1
$haveSemVer = $null -ne ('semver' -as [type])
}
process {
$semVer = $null
if ($haveSemVer -and ($Version -is [semver])) {
$semVer = $Version
}
elseif ($Version -isnot [version]) {
# Try interpretation as a [semver] first.
if ($haveSemVer -and $Component -ne 'Revision') { $semVer = $Version -as [semver] }
# Fall back to [version]
if (-not $semVer) {
$Version = [version] "$Version"
if (-not $?) { return } # If interpretation as [version] fails, give up.
}
}
if ($semVer) {
# [semver]
if ($Component -eq 'Revision') { Write-Error "[semver] instances don't have a 'Revision' component"; return }
$newNumericComponents = switch ($Component) {
'Major' { (1 + $semVer.Major), 0, 0 }
'Minor' { $semVer.Major, (1 + $semVer.Minor), 0 }
# $null (default), 'Patch' == 'Build'
Default { $semVer.Major, $semVer.Minor, (1 + $semVer.Patch) }
}
[semver]::new(
$newNumericComponents[0],
$newNumericComponents[1],
$newNumericComponents[2],
$semVer.PreReleaseLabel,
$semVer.BuildLabel
)
}
else {
# [version]
if ($Component -eq 'Patch') { $Component = 'Build' }
$newComponents = switch ($Component) {
'Major' { (1 + $Version.Major), 0 }
'Minor' { $Version.Major, (1 + $Version.Minor) }
'Build' { $Version.Major, $Version.Minor, (1 + [Math]::Max(0, $Version.Build)) }
# $null (default), 'Revision'
Default { $Version.Major, $Version.Minor, [Math]::Max(0, $Version.Build), (1 + [Math]::Max(0, $Version.Revision)) }
}
if ($newComponents.Count -eq 2 -and $Version.Build -ne -1) {
$newComponents += 0
}
if ($newComponents.Count -eq 3 -and $Version.Revision -ne -1) {
$newComponents += 0
}
[version]::new.Invoke($newComponents)
}
}
}