powershellversion-controlversionincrementtext-processing

Increment a version number contained in a text file


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.:


Solution

  • In 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:

    Note:


    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)
        }
    
      }
    
    }