Skip to content

Commit

Permalink
feat: support all semver versions
Browse files Browse the repository at this point in the history
  • Loading branch information
Thilas committed Jun 19, 2023
1 parent 7d06d93 commit 2a21db0
Show file tree
Hide file tree
Showing 5 changed files with 458 additions and 182 deletions.
128 changes: 78 additions & 50 deletions AU/Private/AUVersion.ps1
Original file line number Diff line number Diff line change
@@ -1,56 +1,76 @@
enum SemVer {
V1
V2
EnhancedV2
}

class AUVersion : System.IComparable {
[version] $Version
[string] $Prerelease
[string] $BuildMetadata

AUVersion([version] $version, [string] $prerelease, [string] $buildMetadata) {
hidden AUVersion([version] $version, [string] $prerelease, [string] $buildMetadata) {
if (!$version) { throw 'Version cannot be null.' }
$this.Version = [AUVersion]::NormalizeVersion($version)
$this.Prerelease = [AUVersion]::NormalizePrerelease($prerelease) -join '.'
$this.Version = [AUVersion]::NormalizeVersion($version)
$this.Prerelease = [AUVersion]::NormalizePrerelease($prerelease) -join '.'
$this.BuildMetadata = $buildMetadata
}

AUVersion($input) {
if (!$input) { throw 'Input cannot be null.' }
$v = [AUVersion]::Parse($input -as [string])
$this.Version = $v.Version
$this.Prerelease = $v.Prerelease
AUVersion($value) {
if (!$value) { throw 'Input cannot be null.' }
$v = [AUVersion]::Parse($value -as [string])
$this.Version = $v.Version
$this.Prerelease = $v.Prerelease
$this.BuildMetadata = $v.BuildMetadata
}

static [AUVersion] Parse([string] $input) { return [AUVersion]::Parse($input, $true) }
static [AUVersion] Parse([string] $value) {
return [AUVersion]::Parse($value, $true)
}

static [AUVersion] Parse([string] $value, [bool] $strict) {
return [AUVersion]::Parse($value, $strict, [SemVer]::V2)
}

static [AUVersion] Parse([string] $value, [bool] $strict, [SemVer] $semver) {
if (!$value) { throw 'Version cannot be null.' }
$v = [ref] $null
if (![AUVersion]::TryParse($value, $v, $strict, $semver)) {
throw "Invalid SemVer $semver version: `"$value`"."
}
return $v.Value
}

static [AUVersion] Parse([string] $input, [bool] $strict) {
if (!$input) { throw 'Version cannot be null.' }
$reference = [ref] $null
if (![AUVersion]::TryParse($input, $reference, $strict)) { throw "Invalid version: $input." }
return $reference.Value
static [bool] TryParse([string] $value, [ref] $result) {
return [AUVersion]::TryParse($value, $result, $true)
}

static [bool] TryParse([string] $input, [ref] $result) { return [AUVersion]::TryParse($input, $result, $true) }
static [bool] TryParse([string] $value, [ref] $result, [bool] $strict) {
return [AUVersion]::TryParse($value, $result, $strict, [SemVer]::V2)
}

static [bool] TryParse([string] $input, [ref] $result, [bool] $strict) {
static [bool] TryParse([string] $value, [ref] $result, [bool] $strict, [SemVer] $semver) {
$result.Value = [AUVersion] $null
if (!$input) { return $false }
if (!$value) { return $false }
$pattern = [AUVersion]::GetPattern($strict)
if ($input -notmatch $pattern) { return $false }
$reference = [ref] $null
if (![version]::TryParse($Matches.version, $reference)) { return $false }
$pr = $Matches.prerelease
$bm = $Matches.buildMetadata
if ($pr -and !$strict) { $pr = [AUVersion]::RefineParts($pr) }
if ($bm -and !$strict) { $bm = [AUVersion]::RefineParts($bm) }
$result.Value = [AUVersion]::new($reference.Value, $pr, $bm)
if ($value -notmatch $pattern) { return $false }
$v = [ref] $null
if (![version]::TryParse($Matches.version, $v)) { return $false }
$pr = [ref] $null
if (![AUVersion]::TryRefineIdentifiers($Matches.prerelease, $pr, $strict, $semver)) { return $false }
$bm = [ref] $null
if (![AUVersion]::TryRefineIdentifiers($Matches.buildMetadata, $bm, $strict, $semver)) { return $false }
$result.Value = [AUVersion]::new($v.Value, $pr.Value, $bm.Value)
return $true
}

hidden static [version] NormalizeVersion([version] $value) {
if ($value.Revision -eq 0) {
return $value.ToString(3)
}
if ($value.Build -eq -1) {
return [version] "$value.0"
}
if ($value.Revision -eq 0) {
return [version] $value.ToString(3)
}
return $value
}

Expand All @@ -59,7 +79,7 @@ class AUVersion : System.IComparable {
if ($value) {
$value -split '\.' | ForEach-Object {
# if identifier is exclusively numeric, cast it to an int
if ($_ -match '^[0-9]+$') {
if ($_ -match '^\d+$') {
$result += [int] $_
} else {
$result += $_
Expand All @@ -75,27 +95,39 @@ class AUVersion : System.IComparable {
$identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*"
return "^$versionPattern(?:-(?<prerelease>$identifierPattern))?(?:\+(?<buildMetadata>$identifierPattern))?`$"
} else {
$identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+| \d+)*"
return "$versionPattern(?:[- ]*(?<prerelease>$identifierPattern))?(?:[+ *](?<buildMetadata>$identifierPattern))?"
$identifierPattern = "[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+| +\d+)*"
return "$versionPattern(?:(?:-| *)(?<prerelease>$identifierPattern))?(?:(?:\+| *)(?<buildMetadata>$identifierPattern))?"
}
}

hidden static [string] RefineParts([string] $value) {
$result = if ($value -match '^(?<identifier>[A-Za-z]+)(?<digits>\d+)$') {
'{0}.{1}' -f $Matches.identifier, $Matches.digits
} else {
$value.Replace(' ', '.')
hidden static [bool] TryRefineIdentifiers([string] $value, [ref] $result, [bool] $strict, [SemVer] $semver) {
$result.Value = [string] ''
if (!$value) { return $true }
if (!$strict) { $value = $value -replace ' +', '.' }
if ($semver -eq [SemVer]::V1) {
# SemVer1 means no dot-separated identifiers
if ($strict -and $value -match '\.') { return $false }
$value = $value.Replace('.', '')
} elseif ($semver -eq [SemVer]::EnhancedV2) {
# Try to improve a SemVer1 version into a SemVer2 one
# e.g. 1.24.0-beta2 becomes 1.24.0-beta.2
if ($value -match '^(?<identifier>[A-Za-z-]+)(?<digits>\d+)$') {
$value = '{0}.{1}' -f $Matches.identifier, $Matches.digits
}
}
return $result
$result.Value = $value
return $true
}

[AUVersion] WithVersion([version] $version) { return [AUVersion]::new($version, $this.Prerelease, $this.BuildMetadata) }
[AUVersion] WithVersion([version] $version) {
return [AUVersion]::new($version, $this.Prerelease, $this.BuildMetadata)
}

[int] CompareTo($obj) {
if ($obj -eq $null) { return 1 }
if ($obj -isnot [AUVersion]) { throw "AUVersion expected: $($obj.GetType())" }
$t = $this.GetAllParts()
$o = $obj.GetAllParts()
if ($null -eq $obj) { return 1 }
if ($obj -isnot [AUVersion]) { throw "[AUVersion] expected, got [$($obj.GetType())]." }
$t = $this.GetParts()
$o = $obj.GetParts()
for ($i = 0; $i -lt $t.Length -and $i -lt $o.Length; $i++) {
if ($t[$i].GetType() -ne $o[$i].GetType()) {
$t[$i] = [string] $t[$i]
Expand All @@ -113,12 +145,12 @@ class AUVersion : System.IComparable {

[bool] Equals($obj) { return $this.CompareTo($obj) -eq 0 }

[int] GetHashCode() { return $this.GetAllParts().GetHashCode() }
[int] GetHashCode() { return $this.GetParts().GetHashCode() }

[string] ToString() {
$result = $this.Version.ToString()
if ($this.Prerelease) { $result += "-$($this.Prerelease)" }
if ($this.BuildMetadata) { $result += "+$($this.BuildMetadata)" }
if ($this.Prerelease) { $result += '-{0}' -f $this.Prerelease }
if ($this.BuildMetadata) { $result += '+{0}' -f $this.BuildMetadata }
return $result
}

Expand All @@ -127,13 +159,9 @@ class AUVersion : System.IComparable {
return $this.Version.ToString($fieldCount)
}

hidden [object[]] GetAllParts() {
hidden [object[]] GetParts() {
$result = , $this.Version
$result += [AUVersion]::NormalizePrerelease($this.Prerelease)
return $result
}
}

function ConvertTo-AUVersion($Version) {
return [AUVersion] $Version
}
46 changes: 34 additions & 12 deletions AU/Public/Get-Version.ps1
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Author: Thomas Démoulins <[email protected]>
# Author: Thomas Démoulins <[email protected]>

<#
.SYNOPSIS
Parses a semver-like object from a string in a flexible manner.
Parses a SemVer-like object from a string in a flexible manner.
.DESCRIPTION
This function parses a string containing a semver-like version
This function parses a string containing a SemVer-like version
and returns an object that represents both the version (with up to 4 parts)
and optionally a pre-release and a build metadata.
Expand All @@ -16,26 +16,48 @@
- extra spaces are ignored
- optional delimiters can be provided to help parsing the string
Parameter -SemVer allows to specify the max supported SemVer version:
V1 (default) or V2 (requires choco v2.0.0). EnhancedV2 is about
transforming a SemVer1-like version into a SemVer2-like one when
possible (e.g. 1.61.0-beta.0 instead of 1.61.0-beta0).
Resulting version is normalized the same way chocolatey/nuget does.
See https://learn.microsoft.com/en-us/nuget/concepts/package-versioning#normalized-version-numbers
.EXAMPLE
Get-Version 'Current version 2.1.1 beta 2.'
Returns 2.1.1-beta2
.EXAMPLE
Get-Version -SemVer V2 'Current version 2.1.1 beta 2.'
Returns 2.1.1-beta.2
.EXAMPLE
Get-Version 'Last version: 1.2.3 beta 3.'
Get-Version -SemVer V2 '4.0.3Beta1'
Returns 1.2.3-beta.3
Returns 4.0.3-Beta1
.EXAMPLE
Get-Version 'https://github.com/atom/atom/releases/download/v1.24.0-beta2/AtomSetup.exe'
Get-Version -SemVer EnhancedV2 '4.0.3Beta1'
Return 1.24.0-beta.2
Returns 4.0.3-Beta.1
.EXAMPLE
Get-Version 'http://mirrors.kodi.tv/releases/windows/win32/kodi-17.6-Krypton-x86.exe' -Delimiter '-'
Get-Version 'https://dl.airserver.com/pc32/AirServer-5.6.3-x86.msi' -Delimiter '-'
Return 17.6
Returns 5.6.3
#>
function Get-Version {
[CmdletBinding()]
param(
# Supported SemVer version: V1 (default) or V2 (requires choco v2.0.0).
# EnhancedV2 allows to transform a SemVer1-like version into SemVer2-like one (e.g. 1.2.0-rc.3 instead of 1.2.0-rc3)
[ValidateSet('V1', 'V2', 'EnhancedV2')]
[string] $SemVer = 'V1',
# Version string to parse.
[Parameter(Mandatory=$true)]
[Parameter(Mandatory, Position=0)]
[string] $Version,
# Optional delimiter(s) to help locate the version in the string: the version must start and end with one of these chars.
[char[]] $Delimiter
Expand All @@ -46,10 +68,10 @@ function Get-Version {
$regex = $Version | Select-String -Pattern "[$delimiters](\d+\.\d+[^$delimiters]*)[$delimiters]" -AllMatches
foreach ($match in $regex.Matches) {
$reference = [ref] $null
if ([AUVersion]::TryParse($match.Groups[1], $reference, $false)) {
if ([AUVersion]::TryParse($match.Groups[1], $reference, $false, $SemVer)) {
return $reference.Value
}
}
}
return [AUVersion]::Parse($Version, $false)
return [AUVersion]::Parse($Version, $false, $SemVer)
}
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,13 +346,13 @@ In order to help working with versions, function `Get-Version` can be called in
- To force the update of the single stream, use `IncludeStream` parameter. To do so via commit message, use `[AU package\stream]` syntax.

```powershell
PS> Get-Version 'v1.3.2.7rc1'
PS> Get-Version 'v1.3.2.7rc.1'
Version Prerelease BuildMetadata
------- ---------- -------------
1.3.2.7 rc.1
1.3.2.7 rc1
PS> $version = Get-Version '1.3.2-beta.2+5'
PS> $version = Get-Version -SemVer V2 '1.3.2-beta.2+5'
PS> $version.ToString(2) + ' => ' + $version.ToString()
1.3 => 1.3.2-beta.2+5
```
Expand Down
Loading

0 comments on commit 2a21db0

Please sign in to comment.