PSSemVer.psm1
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidLongLines', '', Justification = 'Contains long links.')] [CmdletBinding()] param() if ($PSVersionTable.PSVersion -lt '6.0') { [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSAvoidAssignmentToAutomaticVariable', '', Justification = 'Compatibility with PowerShell 6.0 and newer.' )] $IsWindows = [System.Environment]::OSVersion.Platform -eq 'Win32NT' } $scriptName = 'PSSemVer' Write-Verbose "[$scriptName] - Importing module" #region - From [classes] Write-Verbose "[$scriptName] - [classes] - Processing folder" #region - From [classes] - [PSSemVer] Write-Verbose "[$scriptName] - [classes] - [PSSemVer] - Importing" class PSSemVer : System.Object, System.IComparable, System.IEquatable[Object] { #region Static properties hidden static [string] $PSSemVerPattern = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)' + '(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' hidden static [string] $LoosePSSemVerPattern = '^(?:([a-zA-Z]*)-?)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)' + '(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$' #endregion Static properties #region Properties [string] $Prefix [int] $Major [int] $Minor [int] $Patch [string] $Prerelease [string] $BuildMetadata #endregion Properties #region Constructors PSSemVer() {} PSSemVer([int]$Major) { $this.Major = $Major } PSSemVer([int]$Major, [int]$Minor) { $this.Major = $Major $this.Minor = $Minor } PSSemVer([int]$Major, [int]$Minor, [int]$Patch) { $this.Major = $Major $this.Minor = $Minor $this.Patch = $Patch } PSSemVer([int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel) { $this.Major = $Major $this.Minor = $Minor $this.Patch = $Patch $this.Prerelease = $PreReleaseLabel } PSSemVer([int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel, [string]$BuildLabel) { $this.Major = $Major $this.Minor = $Minor $this.Patch = $Patch $this.Prerelease = [string]$PreReleaseLabel $this.BuildMetadata = $BuildLabel } PSSemVer([string]$Prefix, [int]$Major) { $this.Prefix = $Prefix $this.Major = $Major } PSSemVer([string]$Prefix, [int]$Major, [int]$Minor) { $this.Prefix = $Prefix $this.Major = $Major $this.Minor = $Minor } PSSemVer([string]$Prefix, [int]$Major, [int]$Minor, [int]$Patch) { $this.Prefix = $Prefix $this.Major = $Major $this.Minor = $Minor $this.Patch = $Patch } PSSemVer([string]$Prefix, [int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel) { $this.Prefix = $Prefix $this.Major = $Major $this.Minor = $Minor $this.Patch = $Patch $this.Prerelease = $PreReleaseLabel } PSSemVer([string]$Prefix, [int]$Major, [int]$Minor, [int]$Patch, [string]$PreReleaseLabel, [string]$BuildLabel) { $this.Prefix = $Prefix $this.Major = $Major $this.Minor = $Minor $this.Patch = $Patch $this.Prerelease = $PreReleaseLabel $this.BuildMetadata = $BuildLabel } PSSemVer([string]$version) { if ($version -match [PSSemVer]::PSSemVerPattern) { Write-Debug 'PSSemVerPattern' $this.Major = [int]$Matches[1] $this.Minor = [int]$Matches[2] $this.Patch = [int]$Matches[3] $this.Prerelease = $Matches[4] $this.BuildMetadata = $Matches[5] } elseif ($version -match [PSSemVer]::LoosePSSemVerPattern) { Write-Debug 'LoosePSSemVerPattern' $this.Prefix = $Matches[1] $this.Major = [int]$Matches[2] $this.Minor = [int]$Matches[3] $this.Patch = [int]$Matches[4] $this.Prerelease = $Matches[5] $this.BuildMetadata = $Matches[6] } else { throw [ArgumentException]::new('The version string is not a valid SemVer string') } } PSSemVer([version]$version) { $this.Major = if ($version.Major -lt 0) { 0 } else { $version.Major } $this.Minor = if ($version.Minor -lt 0) { 0 } else { $version.Minor } $this.Patch = if ($version.Build -lt 0) { 0 } else { $version.Build } } #endregion Constructors #region Methods [void] BumpMajor() { $this.Major = $this.Major + 1 $this.Minor = 0 $this.Patch = 0 } [void] BumpMinor() { $this.Minor = $this.Minor + 1 $this.Patch = 0 } [void] BumpPatch() { $this.Patch = $this.Patch + 1 } [void] BumpPrereleaseNumber() { if (-not [string]::IsNullOrEmpty($this.Prerelease)) { $PrereleaseNumber = [PSSemVer]::GetPrereleaseNumber($this.Prerelease) if ([string]::IsNullOrEmpty($PrereleaseNumber)) { $PrereleaseNumber = 1 } else { $PrereleaseNumber++ } $this.PreRelease = "$([PSSemVer]::GetPrereleaseLabel($this.Prerelease)).$PrereleaseNumber" } } [void] SetPrerelease([string]$label) { $this.Prerelease = $label } [void] SetPrereleaseLabel([string]$label) { $this.SetPrerelease($label) } [void] SetBuild([string]$label) { $this.SetBuildMetadata($label) } [void] SetBuildLabel([string]$label) { $this.SetBuildMetadata($label) } [void] SetBuildMetadata([string]$label) { $this.BuildMetadata = $label } [int] CompareTo([Object]$other) { if (-not $other -is [PSSemVer]) { throw [ArgumentException]::new('The argument must be of type PSSemVer') } if ($this.Major -lt $other.Major) { return -1 } if ($this.Major -gt $other.Major) { return 1 } if ($this.Minor -lt $other.Minor) { return -1 } if ($this.Minor -gt $other.Minor) { return 1 } if ($this.Patch -lt $other.Patch) { return -1 } if ($this.Patch -gt $other.Patch) { return 1 } if ([string]::IsNullOrEmpty($this.Prerelease) -and [string]::IsNullOrEmpty($other.Prerelease)) { return 0 } if ([string]::IsNullOrEmpty($this.Prerelease)) { return 1 } if ([string]::IsNullOrEmpty($other.Prerelease)) { return -1 } $thisPrereleaseArray = ($this.Prerelease -split '\.') $otherPrereleaseArray = ($other.Prerelease -split '\.') for ($i = 0; $i -lt [Math]::Max($thisPrereleaseArray.Length, $otherPrereleaseArray.Length); $i++) { if ($i -ge $thisPrereleaseArray.Length) { return -1 } if ($i -ge $otherPrereleaseArray.Length) { return 1 } if ($thisPrereleaseArray[$i] -eq $otherPrereleaseArray[$i]) { continue } if ($thisPrereleaseArray[$i] -match '^\d+$' -and $otherPrereleaseArray[$i] -match '^\d+$') { return [int]$thisPrereleaseArray[$i] - [int]$otherPrereleaseArray[$i] } if ($thisPrereleaseArray[$i] -match '^\d+$' -and $otherPrereleaseArray[$i] -notmatch '^\d+$') { return -1 } if ($thisPrereleaseArray[$i] -notmatch '^\d+$' -and $otherPrereleaseArray[$i] -match '^\d+$') { return 1 } return $thisPrereleaseArray[$i].CompareTo($otherPrereleaseArray[$i]) } return 0 } [bool] Equals([Object]$other) { if (-not $other -is [PSSemVer]) { return $false } if ($this.Major -ne $other.Major) { return $false } if ($this.Minor -ne $other.Minor) { return $false } if ($this.Patch -ne $other.Patch) { return $false } if ($this.Prerelease -ne $other.Prerelease) { return $false } if ($this.BuildMetadata -ne $other.BuildMetadata) { return $false } return $true } [string] ToString() { [string]$output = "$($this.Prefix)$($this.Major).$($this.Minor).$($this.Patch)" if (-not [string]::IsNullOrEmpty($this.Prerelease)) { $output += "-$($this.Prerelease)" } if (-not [string]::IsNullOrEmpty($this.BuildMetadata)) { $output += "+$($this.BuildMetadata)" } return $output } #endregion Methods #region Static Methods static [PSSemVer] Parse([string]$string) { return [PSSemVer]::new($string) } static [Nullable[int]] GetPrereleaseNumber([string]$string) { if ($string -match '^(.*?)(?:\.(\d+))?$') { return [Nullable[int]]$matches[2] } return $null } static [string] GetPrereleaseLabel([string]$string) { if ($string -match '^(.*?)(?:\.(\d+))?$') { return $matches[1] } return $null } #endregion Static Methods } Write-Verbose "[$scriptName] - [classes] - [PSSemVer] - Done" #endregion - From [classes] - [PSSemVer] Write-Verbose "[$scriptName] - [classes] - Done" #endregion - From [classes] #region - From [public] Write-Verbose "[$scriptName] - [public] - Processing folder" #region - From [public] - [ConvertTo-PSSemVer] Write-Verbose "[$scriptName] - [public] - [ConvertTo-PSSemVer] - Importing" filter ConvertTo-PSSemVer { <# .SYNOPSIS Converts a version string to a PSSemVer object. .DESCRIPTION This function takes a version string and converts it to a PSSemVer object. .EXAMPLE '1.2.3-alpha.1+001' | ConvertTo-SemVer Major : 1 Minor : 2 Patch : 3 Prerelease : alpha.1 BuildMetadata : 001 .NOTES Compatible with SemVer 2.0.0. #> [OutputType([PSSemVer])] [CmdletBinding()] param ( # The version to convert. [Parameter( Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName) ] [AllowNull()] [AllowEmptyString()] [string] $Version ) if ([string]::IsNullOrEmpty($Version)) { return New-PSSemVer } try { $PSSemVer = [PSSemVer]::new($Version) return $PSSemVer } catch { throw "Failed to convert '$Version' to PSSemVer." } } Write-Verbose "[$scriptName] - [public] - [ConvertTo-PSSemVer] - Done" #endregion - From [public] - [ConvertTo-PSSemVer] #region - From [public] - [New-PSSemVer] Write-Verbose "[$scriptName] - [public] - [New-PSSemVer] - Importing" function New-PSSemVer { <# .SYNOPSIS Creates a new PSSemVer object. .DESCRIPTION This function creates a new PSSemVer object. .EXAMPLE New-SemVer -Version '1.2.3-alpha.1+001' Major : 1 Minor : 2 Patch : 3 Prerelease : alpha.1 BuildMetadata : 001 .EXAMPLE New-SemVer -Major 1 -Minor 2 -Patch 3 -Prerelease 'alpha.1' -BuildMetadata '001' Major : 1 Minor : 2 Patch : 3 Prerelease : alpha.1 BuildMetadata : 001 .NOTES Compatible with SemVer 2.0.0. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute( 'PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Does not change system state, but creates a new object.' )] [OutputType([PSSemVer])] [CmdletBinding(DefaultParameterSetName = 'Values')] param ( # The major version. [Parameter(ParameterSetName = 'Values')] [int] $Major = 0, # The minor version. [Parameter(ParameterSetName = 'Values')] [int] $Minor = 0, # The patch version. [Parameter(ParameterSetName = 'Values')] [int] $Patch = 0, # The prerelease version. [Parameter(ParameterSetName = 'Values')] [Alias('PreReleaseLabel')] [string] $Prerelease = '', # The build metadata. [Parameter(ParameterSetName = 'Values')] [Alias('Build', 'BuildLabel')] [string] $BuildMetadata = '', # The version as a string. [Parameter( Mandatory, ParameterSetName = 'String' )] [AllowEmptyString()] [string] $Version ) switch ($PSCmdlet.ParameterSetName) { 'Values' { return [PSSemVer]::New($Major, $Minor, $Patch, $Prerelease, $BuildMetadata) } 'String' { if ([string]::IsNullOrEmpty($Version)) { $Version = '0.0.0' } return [PSSemVer]::New($Version) } } } Write-Verbose "[$scriptName] - [public] - [New-PSSemVer] - Done" #endregion - From [public] - [New-PSSemVer] Write-Verbose "[$scriptName] - [public] - Done" #endregion - From [public] # Get the internal TypeAccelerators class to use its static methods. $TypeAcceleratorsClass = [psobject].Assembly.GetType( 'System.Management.Automation.TypeAccelerators' ) # Ensure none of the types would clobber an existing type accelerator. # If a type accelerator with the same name exists, throw an exception. $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get # Define the types to export with type accelerators. $ExportableEnums = @( ) $ExportableEnums | Foreach-Object { Write-Verbose "Exporting enum '$($_.FullName)'." } foreach ($Type in $ExportableEnums) { if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { Write-Warning "Enum already exists [$($Type.FullName)]. Skipping." } else { Write-Verbose "Importing enum '$Type'." $TypeAcceleratorsClass::Add($Type.FullName, $Type) } } $ExportableClasses = @( [PSSemVer] ) $ExportableClasses | Foreach-Object { Write-Verbose "Exporting class '$($_.FullName)'." } foreach ($Type in $ExportableClasses) { if ($Type.FullName -in $ExistingTypeAccelerators.Keys) { Write-Warning "Class already exists [$($Type.FullName)]. Skipping." } else { Write-Verbose "Importing class '$Type'." $TypeAcceleratorsClass::Add($Type.FullName, $Type) } } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in ($ExportableEnums + $ExportableClasses)) { $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure() $exports = @{ Alias = '*' Cmdlet = '' Function = @( 'ConvertTo-PSSemVer' 'New-PSSemVer' ) Variable = '' } Export-ModuleMember @exports |