public/New-SemanticVersion.ps1
function New-SemanticVersion { <# .SYNOPSIS Creates a new semantic version. .DESCRIPTION Creates a new object representing a semantic version number. .EXAMPLE New-SemanticVersion -String '1.2.3-alpha.4+build.5' Major : 1 Minor : 2 Patch : 3 PreRelease : alpha.4 Build : build.5 This command converts a valid Semantic Version string into a Semantic Version object. The output of the command is a Semantic Version object with the elements of the version split into separate properties. .EXAMPLE New-SemanticVersion -Major 1 -Minor 2 -Patch 3 -PreRelease alpha.4 -Build build.5 Major : 1 Minor : 2 Patch : 3 PreRelease : alpha.4 Build : build.5 This command takes the Major, Minor, Patch, PreRelease, and Build parameters and produces the same output as the previous example. .EXAMPLE New-SemanticVersion -Major 1 -Minor 2 -Patch 3 -PreRelease alpha, 4 -Build build, 5 Major : 1 Minor : 2 Patch : 3 PreRelease : alpha.4 Build : build.5 This command uses arrays for the PreRelease and Build parameters, but produces the same output as the previous example. .EXAMPLE $semver = New-SemanticVersion -Major 1 -Minor 2 -Patch 3 -PreRelease alpha.4 -Build build.5 $semver.ToString() 1.2.3-alpha.4+build.5 This example shows that the object output from the previous command can be saved to a variable. Then by calling the object's ToString() method, a valid Semantic Version string is returned. .INPUTS System.Object All Objects piped to this function are converted into Semantic Version objects. #> [CmdletBinding(DefaultParameterSetName='Elements')] [Alias('nsemver')] [OutputType('PoshSemanticVersion')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] param ( # The major version must be incremented if any backwards incompatible changes are introduced to the public API. [Parameter(ParameterSetName='Elements')] [ValidateRange(0, 2147483647)] [int] $Major = 0, # The minor version must be incremented if new, backwards compatible functionality is introduced to the public API. [Parameter(ParameterSetName='Elements')] [ValidateRange(0, 2147483647)] [int] $Minor = 0, # The patch version must be incremented if only backwards compatible bug fixes are introduced. [Parameter(ParameterSetName='Elements')] [ValidateRange(0, 2147483647)] [int] $Patch = 0, # A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility # requirements as denoted by its associated normal version. # The value can be a string or an array of strings. If an array of strings is provided, the elements of the array # will be joined using dot separators. [Parameter(ParameterSetName='Elements')] [AllowEmptyCollection()] $PreRelease = @(), # The build metadata. # The value can be a string or an array of strings. If an array of strings is provided, the elements of the array # will be joined using dot separators. [Parameter(ParameterSetName='Elements')] [AllowEmptyCollection()] $Build = @(), # A valid semantic version string to be converted into a SemanticVersion object. [Parameter(ParameterSetName='String', ValueFromPipeline=$true, Mandatory=$true, Position=0)] [ValidateScript({ [int] $tmpInt = 0 [decimal] $tmpDecimal = 0.0 if ([int]::TryParse($_.ToString(), [ref] $tmpInt)) { $paramValue = '{0}.0.0' -f $tmpInt } elseif ([decimal]::TryParse($_.ToString(), [ref] $tmpDecimal)) { $paramValue = '{0}.0' -f $tmpDecimal } else { $paramValue = $_ } if (Test-SemanticVersion -InputObject $paramValue) { return $true } else { $erHash = Debug-SemanticVersion -InputObject $paramValue -ParameterName InputObject $er = Write-Error @erHash 2>&1 throw ($er) } })] [Alias('Version', 'v', 'String')] [object[]] $InputObject ) begin { [scriptblock] $semVerDynamicModuleScriptBlock = { [CmdletBinding()] param ( # An unsigned int. [ValidateRange(0, 2147483647)] [int] $Major = 0, # An unsigned int. [ValidateRange(0, 2147483647)] [int] $Minor = 0, # An unsigned int. [ValidateRange(0, 2147483647)] [int] $Patch = 0, # A string. [Parameter(Mandatory=$true)] [ValidateScript({$($_ -match '^(0|(\d*[A-Z-]+|[1-9A-Z-])[\dA-Z-]*)$')})] [string[]] $PreRelease = @(), # A string. [ValidateScript({$($_ -match '^[\dA-Z-]+$')})] [string[]] $Build = @() ) New-Variable -Option Constant -Name customObjectTypeName -Value PoshSemanticVersion New-Variable -Option Constant -Name PreReleaseIdRegEx -Value '^(0|(\d*[A-Z-]+|[1-9A-Z-])[\dA-Z-]*)$' New-Variable -Option Constant -Name PreReleaseRegEx -Value '^(|(0|[1-9][0-9]*|[0-9]+[A-Za-z-]+[0-9A-Za-z-]*|[A-Za-z-]+[0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]+[A-Za-z-]+[0-9A-Za-z-]*|[A-Za-z-]+[0-9A-Za-z-]*))*)$' New-Variable -Option Constant -Name BuildIdRegEx -Value '^[\dA-Z-]+$' New-Variable -Option Constant -Name BuildRegEx -Value '^(|([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))$' New-Variable -Option Constant -Name SemVerRegEx -Value $( '^(0|[1-9]\d*)' + '(\.(0|[1-9]\d*)){2}' + '(-(0|(\d*[A-Z-]+|[1-9A-Z-])[\dA-Z-]*)(\.(0|(\d*[A-Z-]+|[1-9A-Z-])[\dA-Z-]*))*)?' + '(\+[\dA-Z-]*(\.[\dA-Z-]*)?)?' + '(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' ) New-Variable -Option Constant -Name NamedSemVerRegEx -Value $( '^(?<major>(0|[1-9][0-9]*))' + '\.(?<minor>(0|[1-9][0-9]*))' + '\.(?<patch>(0|[1-9][0-9]*))' + '(-(?<prerelease>(0|[1-9][0-9]*|[0-9]+[A-Za-z-]+[0-9A-Za-z-]*|[A-Za-z-]+[0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]+[A-Za-z-]+[0-9A-Za-z-]*|[A-Za-z-]+[0-9A-Za-z-]*))*))?' + '(\+(?<build>[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$' ) [System.Collections.Generic.List[string]] $exportedFunctions = [Activator]::CreateInstance([System.Collections.Generic.List[string]]) $exportedFunctions.Add('CompareTo') function CompareTo { <# .SYNOPSIS Compare this SemVerObj to another. .DESCRIPTION Returns 0 if both objects are equal Returns 1 if this object is a higher precedence than the other. Returns -1 if this object is a lower precedence than the other. #> [CmdletBinding()] [OutputType([int])] param ( # The number to be incremented. [Parameter(Mandatory=$true)] [ValidateScript({ $(@($_.pstypenames) -contains $customObjectTypeName) })] [psobject] $Version ) [int] $returnValue = 0 if ($Major -gt $Version.Major) { $returnValue = 1 } elseif ($Major -lt $Version.Major) { $returnValue = -1 } if ($returnValue -eq 0) { if ($Minor -gt $Version.Minor) { $returnValue = 1 } elseif ($Minor -lt $Version.Minor) { $returnValue = -1 } } if ($returnValue -eq 0) { if ($Patch -gt $Version.Patch) { $returnValue = 1 } elseif ($Patch -lt $Version.Patch) { $returnValue = -1 } } if ($returnValue -eq 0 -and ($PreRelease.Length -ne 0 -or ($Version.GetPreRelease().Length -ne 0))) { if ($PreRelease.Length -eq 0 -and ($Version.GetPreRelease().Length -ne 0)) { $returnValue = 1 } elseif ($PreRelease.Length -ne 0 -and ($Version.GetPreRelease().Length -eq 0)) { $returnValue = -1 } } if ($returnValue -eq 0) { [string[]] $VersionPreRelease = $Version.GetPreRelease() [int] $shortestArray = $PreRelease.Length if ($shortestArray -gt $VersionPreRelease.Length) { $shortestArray = $VersionPreRelease.Length } for ([int] $i = 0; $i -lt $shortestArray; $i++) { if ($PreRelease[$i] -match '^[0-9]+$' -and ($VersionPreRelease[$i] -match '^[0-9]+$')) { if (([int] $PreRelease[$i]) -gt ([int] $VersionPreRelease[$i])) { $returnValue = 1 } elseif (([int] $PreRelease[$i]) -lt ([int] $VersionPreRelease[$i])) { $returnValue = -1 } } elseif ($PreRelease[$i] -notmatch '^[0-9]+$' -and ($VersionPreRelease[$i] -match '^[0-9]+$')) { $returnValue = 1 } elseif ($PreRelease[$i] -match '^[0-9]+$' -and ($VersionPreRelease[$i] -notmatch '^[0-9]+$')) { $returnValue = -1 } elseif ($PreRelease[$i] -gt $VersionPreRelease[$i]) { $returnValue = 1 } elseif ($PreRelease[$i] -lt $VersionPreRelease[$i]) { $returnValue = -1 } if ($returnValue -ne 0) { break } } if ($returnValue -eq 0) { if ($PreRelease.Length -gt $VersionPreRelease.Length) { $returnValue = 1 } elseif ($PreRelease.Length -lt $VersionPreRelease.Length) { $returnValue = -1 } } } $returnValue } function CompareVersions { <# .SYNOPSIS Compare this version with a new string. .DESCRIPTION This is an internal implementation of CompareTo that does not require the Typename name to match. Returns 0 if both objects are equal Returns 1 if this object is a higher precedence than the other. Returns -1 if this object is a lower precedence than the other. #> [CmdletBinding()] [OutputType([int])] param ( # The version to compare against. [Parameter(Mandatory=$true)] [ValidateScript({$($_ -match $NamedSemVerRegEx)})] [string] $DifferenceVersion ) [int] $returnValue = 0 if ($DifferenceVersion -match $NamedSemVerRegEx) { [hashtable] $difHash = @{} $difHash['Major'] = [int] $Matches['major'] $difHash['Minor'] = [int] $Matches['minor'] $difHash['Patch'] = [int] $Matches['patch'] $difHash['PreRelease'] = [string[]] @() $difHash['Build'] = [string[]] @() if ($Matches.ContainsKey('preRelease')) { $difHash['PreRelease'] = [string[]] @($Matches['preRelease'] -split '\.') } if ($Matches.ContainsKey('build')) { $difHash['Bulid'] = [string[]] @($Matches['build'] -split '\.') } } else { throw (New-Object -TypeName System.ArgumentException -ArgumentList @('DifferenceVersion was invalid Semantic Version string.')) } if ($Major -gt $difHash.Major) { $returnValue = 1 } elseif ($Major -lt $difHash.Major) { $returnValue = -1 } if ($returnValue -eq 0) { if ($Minor -gt $difHash.Minor) { $returnValue = 1 } elseif ($Minor -lt $difHash.Minor) { $returnValue = -1 } } if ($returnValue -eq 0) { if ($Patch -gt $difHash.Patch) { $returnValue = 1 } elseif ($Patch -lt $difHash.Patch) { $returnValue = -1 } } if ($returnValue -eq 0 -and ($PreRelease.Length -ne 0 -or ($difHash.PreRelease.Length -ne 0))) { if ($PreRelease.Length -eq 0 -and ($difHash.PreRelease.Length -ne 0)) { $returnValue = 1 } elseif ($PreRelease.Length -ne 0 -and ($difHash.PreRelease.Length -eq 0)) { $returnValue = -1 } } if ($returnValue -eq 0) { [string[]] $VersionPreRelease = $difHash.PreRelease [int] $shortestArray = $PreRelease.Length if ($shortestArray -gt $VersionPreRelease.Length) { $shortestArray = $VersionPreRelease.Length } for ([int] $i = 0; $i -lt $shortestArray; $i++) { if ($PreRelease[$i] -match '^[0-9]+$' -and ($VersionPreRelease[$i] -match '^[0-9]+$')) { if (([int] $PreRelease[$i]) -gt ([int] $VersionPreRelease[$i])) { $returnValue = 1 } elseif (([int] $PreRelease[$i]) -lt ([int] $VersionPreRelease[$i])) { $returnValue = -1 } } elseif ($PreRelease[$i] -notmatch '^[0-9]+$' -and ($VersionPreRelease[$i] -match '^[0-9]+$')) { $returnValue = 1 } elseif ($PreRelease[$i] -match '^[0-9]+$' -and ($VersionPreRelease[$i] -notmatch '^[0-9]+$')) { $returnValue = -1 } elseif ($PreRelease[$i] -gt $VersionPreRelease[$i]) { $returnValue = 1 } elseif ($PreRelease[$i] -lt $VersionPreRelease[$i]) { $returnValue = -1 } if ($returnValue -ne 0) { break } } if ($returnValue -eq 0) { if ($PreRelease.Length -gt $VersionPreRelease.Length) { $returnValue = 1 } elseif ($PreRelease.Length -lt $VersionPreRelease.Length) { $returnValue = -1 } } } $returnValue } $exportedFunctions.Add('CompatibleWith') function CompatibleWith { <# .SYNOPSIS Test if the current version is compatible with the parameter argument version. #> [CmdletBinding()] [OutputType([bool])] param ( # The number to be incremented. [Parameter(Mandatory=$true)] [ValidateScript({$(@($_.pstypenames) -contains $customObjectTypeName)})] [psobject] $Version ) [bool] $isCompatible = $true if ((CompareTo -Version $Version) -eq 0) { $isCompatible = $true } elseif ($Major -eq 0) { $isCompatible = $false } elseif ($Major -ne $Version.Major) { $isCompatible = $false } elseif ($PreRelease.Length -ne 0 -and $Version.GetPreRelease().Length -ne 0) { if ([string]::Join('.', $PreRelease) -ne [string]::Join('.', $Version.GetPreRelease())) { $isCompatible = $false } else { if ($Major -ne $Version.Major) { $isCompatible = $false } if ($Minor -ne $Version.Minor) { $isCompatible = $false } if ($Patch -ne $Version.Patch) { $isCompatible = $false } } } elseif ($PreRelease.Length -ne 0 -or $Version.GetPreRelease().Length -ne 0) { $isCompatible = $false } $isCompatible } $exportedFunctions.Add('Equals') function Equals { <# .SYNOPSIS Determine if this semver object is equal in precedence to another semver object. #> [OutputType([bool])] param ( # The number to be incremented. [Parameter(Mandatory=$true)] [ValidateScript({ if (@($_.pstypenames) -contains 'PoshSemanticVersion') { $true } else { throw 'Input object type must be of type "PoshSemanticVersion".' } })] $Version ) (CompareTo -Version $Version) -eq 0 } $exportedFunctions.Add('GetBuild') function GetBuild { <# .SYNOPSIS Returns the build element as a string array. #> [OutputType([string[]])] param () $Build } $exportedFunctions.Add('GetMajor') function GetMajor { <# .SYNOPSIS Returns the major element of the version. #> [OutputType([int])] param () $Major } $exportedFunctions.Add('GetMinor') function GetMinor { <# .SYNOPSIS Returns the minor element of the version. #> [OutputType([int])] param () $Minor } $exportedFunctions.Add('GetPatch') function GetPatch { <# .SYNOPSIS Returns the patch element of the version. #> [OutputType([int])] param () $Patch } $exportedFunctions.Add('GetPreRelease') function GetPreRelease { <# .SYNOPSIS Returns the prerelease element as a string array. #> [OutputType([string[]])] param () $PreRelease } $exportedFunctions.Add('Increment') function Increment { <# .SYNOPSIS Increments the version by the specifield release level. #> [OutputType([void])] param ( # The type on increment level to perform. [ValidateSet('Build', 'PreRelease', 'PrePatch', 'PreMinor', 'PreMajor', 'Patch', 'Minor', 'Major')] [string] $Level = 'PreRelease', # An optional label that can be used with a Level value of "Build" or any of the "Pre*" Levels. [string] $Label ) [int] $numericValue = 0 if ($PSBoundParameters.ContainsKey('Label')) { switch -Wildcard ($Level) { 'Build' { if ($Label -notmatch $BuildRegEx) { throw (New-Object -TypeName System.ArgumentException -ArgumentList @('Invalid Build label specified.')) } } 'Pre*' { if ($Label -notmatch $PreReleaseRegEx) { throw (New-Object -TypeName System.ArgumentException -ArgumentList @('Invalid PreRelease label specified.')) } } default { Write-Warning -Message 'The Label parameter is only used when combined with a Level parameter value of Build, PreRelease, PreMajor, PreMinor, or PrePatch. It will be ignored.' } } } switch ($Level) { 'Build' { if ($PSBoundParameters.ContainsKey('Label') -and $Label -match $BuildRegEx) { $Script:Build = @($Label -split '\.') } elseif ($Build.Length -eq 0) { $Script:Build = @('0') } else { if (-not ($Build[-1].Length -gt 1 -and $Build[-1] -like '0*') -and [int]::TryParse($Build[-1], [ref] $numericValue)) { $Script:Build[-1] = [string] ++$numericValue } else { $Script:Build += '0' } } } 'PreRelease' { if ($PreRelease.Length -eq 0) { $Script:Patch++ if ($PSBoundParameters.ContainsKey('Label')) { $Script:PreRelease = @($Label -split '\.') } else { $Script:PreRelease = @('0') } } else { if ($PSBoundParameters.ContainsKey('Label')) { # If there is an existing prerelease label, the new label must be of a higher precedence. if ((CompareVersions -DifferenceVersion ('{0}.{1}.{2}-{3}' -f $Major, $Minor, $Patch, $Label)) -lt 0) { $Script:PreRelease = @($Label -split '\.') } else { throw (New-Object -TypeName System.ArgumentOutOfRangeException -ArgumentList @('Label', 'New prerelease label is must be of a higher precedence than existing prerelease label.')) } } elseif (-not ($PreRelease[-1].Length -gt 1 -and $PreRelease[-1] -like '0*') -and [int]::TryParse($PreRelease[-1], [ref] $numericValue)) { $Script:PreRelease[-1] = [string] ++$numericValue } else { $Script:PreRelease += '0' } } } 'PrePatch' { $Script:PreRelease = @() $Script:Patch++ if ($PSBoundParameters.ContainsKey('Label')) { $Script:PreRelease = @($Label -split '\.') } else { $Script:PreRelease = @('0') } } 'PreMinor' { $Script:PreRelease = @() $Script:Patch = 0 $Script:Minor++ if ($PSBoundParameters.ContainsKey('Label')) { $Script:PreRelease = @($Label -split '\.') } else { $Script:PreRelease = @('0') } } 'PreMajor' { $Script:PreRelease = @() $Script:Patch = 0 $Script:Minor = 0 $Script:Major++ if ($PSBoundParameters.ContainsKey('Label')) { $Script:PreRelease = @($Label -split '\.') } else { $Script:PreRelease = @('0') } } 'Patch' { if ($PreRelease.Length -eq 0) { $Script:Patch++ } $Script:PreRelease = @() } 'Minor' { if ($Patch -ne 0 -or $PreRelease.Length -eq 0) { $Script:Minor++ } $Script:PreRelease = @(); $Script:Patch = 0 } 'Major' { if ($Patch -ne 0 -or $Minor -ne 0 -or $PreRelease.Length -eq 0) { $Script:Major++ } $Script:PreRelease = @() $Script:Minor = 0 $Script:Patch = 0 } default { throw ('Invalid release level: {0}' -f $Level) } } } $exportedFunctions.Add('SetBuild') function SetBuild { <# .SYNOPSIS Set the build version #> [CmdletBinding()] [OutputType([void])] param ( # The new build label. [Parameter(Mandatory=$true)] [ValidateScript({$($_ -match $BuildRegEx)})] [string] $Build ) $Script:Build = $Build } $exportedFunctions.Add('ToString') function ToString { <# .SYNOPSIS Return a string representation of this object. #> [OutputType([string])] param () [string] "$Major.$Minor.$Patch$( if ($PreRelease.Length -ne 0) { '-' + [string]::Join('.', $PreRelease) } )$( if ($Build.Length -ne 0) { '+' + [string]::Join('.', $Build) } )" } Export-ModuleMember -Function $exportedFunctions Remove-Variable exportedFunctions } } process { if ($PSCmdlet.ParameterSetName -eq 'Elements') { [string] $badParameterName = 'InputObject' # PSv2 does not initialize $PreRelease or $Build if they were not specifies or if they had empty arrays. # So they have to be reinitialized here if they were not specified. if ($PSBoundParameters.ContainsKey('Build')) { [string] $testBuild = $Build -join '.' if ($testBuild -notmatch ('^' + $BuildPattern + '$')) { $badParameterName = 'Build' } [string[]] $Build = @($testBuild -split '\.') } else { [string[]] $Build = @() } if ($PSBoundParameters.ContainsKey('PreRelease')) { [string] $testPreRelease = $PreRelease -join '.' if ($testPreRelease -notmatch ('^' + $PreReleasePattern + '$')) { $badParameterName = 'PreRelease' } [string[]] $PreRelease = @($testPreRelease -split '\.') } else { [string[]] $PreRelease = @() } [string] $InputObject = "$Major.$Minor.$Patch$(if ($PreRelease.Length -gt 0) {'-' + $($PreRelease -join '.')})$(if ($Build.Length -gt 0) {'+' + $($Build -join '.')})" if (-not $(Test-SemanticVersion -InputObject $InputObject)) { $erHash = Debug-SemanticVersion -InputObject $InputObject -ParameterName $badParameterName $er = Write-Error @erHash 2>&1 $PSCmdlet.ThrowTerminatingError($er) } } foreach ($item in $InputObject) { [int] $tmpInt = 0 [decimal] $tmpDecimal = 0.0 if ([int]::TryParse($item.ToString(), [ref] $tmpInt)) { $paramValue = '{0}.0.0' -f $tmpInt } elseif ([decimal]::TryParse($item.ToString(), [ref] $tmpDecimal)) { $paramValue = '{0}.0' -f $tmpDecimal } else { $paramValue = $item } [hashtable] $semVerHash = Split-SemanticVersion $paramValue.ToString() switch ($semVerHash.Keys) { 'Major' { [int] $Major = $semVerHash['Major'] } 'Minor' { [int] $Minor = $semVerHash['Minor'] } 'Patch' { [int] $Patch = $semVerHash['Patch'] } 'PreRelease' { [string[]] $PreRelease = @($semVerHash['PreRelease']) } 'Build' { [string[]] $Build = @($semVerHash['Build']) } } [psobject] $semVer = New-Module -Name ($customObjectTypeName + 'DynamicModule') -ArgumentList @($Major, $Minor, $Patch, $PreRelease, $Build) -AsCustomObject -ScriptBlock $semVerDynamicModuleScriptBlock $semVer.pstypenames.Insert(0, $customObjectTypeName) $semVer } } } Export-ModuleMember -Function New-SemanticVersion -Alias nsemver |