3

So, I have a list of versions that looks like this:

v1.1.0
v1.2.0
v1.3.0
v1.4.0
v1.5.0
v1.7.0
v1.8.0
v1.9.0
v2.0.0
v2.1.0
v2.10.0
v2.11.0
v2.12.0
v2.2.0
v2.3.0
v2.4.0
v2.5.0
v2.6.0
v2.7.0
v2.8.0
v2.9.0

The problem is, they are ordered incorrectly. I am new to Powershell, so I am having some issues trying to sort them. I tried to do this:

$tags = git tag
$versions = $tags | %{ new-object System.Version ($_) } | sort

But I get this error:

new-object : Exception calling ".ctor" with "1" argument(s): "Version string portion was too short or too long." At line:1 char:24 + $versions = $tags | %{ new-object System.Version ($_) } | sort + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : InvalidOperation: (:) [New-Object], MethodInvocationException + FullyQualifiedErrorId : ConstructorInvokedThrowException,Microsoft.PowerShell.Commands.NewObjectCommand

Can anyone help?


Update

I have used one of the solutions which looks like this:

$location = Get-Location
$path = $location.tostring() + "\CHANGELOG.md"
$tags = git tag
$i = 0

Clear-Content $path
Add-Content $path "Change Log"
Add-Content $path "=========="
Add-Content $path " "

$ToNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20) }) }
$tags | Sort-Object $ToNatural

foreach($tag in $tags) 
{
    if (-NOT ($tag -match "v(\d+\.)(\d+\.)(\*|\d+)")) { continue }
    $i = $i + 1
    if ($i -eq 0) { continue }
    $tag
    If ($i -gt 0) {
        $previous = $tags[$i - 1]

        Add-Content $path " "
    }
}

This sort of works, but all tags seem to be console logged and it shows this:

1.6.0
changeDeliveryFieldAccess
orders/autoComplete
returns/autoComplete
save-lines-dates
services/serviceDetails
tile-colours
users/confirmation
v0.1
v1.1.0
v1.2.0
v1.3.0
v1.4.0
v1.5.0
v1.7.0
v1.8.0
v1.9.0
v2.0.0
v2.1.0
v2.2.0
v2.3.0
v2.4.0
v2.5.0
v2.6.0
v2.7.0
v2.8.0
v2.9.0
v2.10.0
v2.11.0
v2.12.0
v.2.7.1

As you can see, there are a few in there that I don't want. Specifically:

1.6.0
changeDeliveryFieldAccess
orders/autoComplete
returns/autoComplete
save-lines-dates
services/serviceDetails
tile-colours
users/confirmation
v.2.7.1

Once those have been purged from my list, then the order will be right :)


Update 2

So I have tried another solution that is hopefully better:

$location = Get-Location $path = $location.tostring() + "\CHANGELOG.md" $tags = git tag $i = 0

Clear-Content $path
Add-Content $path "#Change Log"
Add-Content $path "=========="
Add-Content $path " "

$tags |
  Where-Object { $_.Substring(1) -as [version] } |
   Sort-Object { [version] $_.Substring(1) }

foreach($tag in $tags) {
    write-host "$($tag) is ok"
}

I am not sure if I am doing this right, but this is the output from the above code:

1.6.0 is ok
changeDeliveryFieldAccess is ok
orders/autoComplete is ok
returns/autoComplete is ok
save-lines-dates is ok
services/serviceDetails is ok
tile-colours is ok
users/confirmation is ok
v.2.7.1 is ok
v0.1 is ok
v1.1.0 is ok
v1.2.0 is ok
v1.3.0 is ok
v1.4.0 is ok
v1.5.0 is ok
v1.7.0 is ok
v1.8.0 is ok
v1.9.0 is ok
v2.0.0 is ok
v2.1.0 is ok
v2.10.0 is ok
v2.11.0 is ok
v2.12.0 is ok
v2.2.0 is ok
v2.3.0 is ok
v2.4.0 is ok
v2.5.0 is ok
v2.6.0 is ok
v2.7.0 is ok
v2.8.0 is ok
v2.9.0 is ok
r3plica
  • 13,017
  • 23
  • 128
  • 290

4 Answers4

7

tl;dr:

You've later indicated that your $tags array also contains other, non-version strings, so these must be filtered out:

$sortedVersionTags = $tags |
  Where-Object { $_.Substring(1) -as [version] } |
    Sort-Object { [version] $_.Substring(1) }
  • Where-Object { $_.Substring(1) -as [version] } only passes those strings through that can be converted to [version] (System.Version) objects - -as [version] - after removing the v at the beginning (.Substring(1); neglecting to remove the v was your initial problem); the -as operator either returns a successfully converted value or $null.

  • Sort-Object then sorts the filtered tags as version numbers, which yields the correct order - see the next section for an explanation.

  • $sortedVersionTags then receives version tags only, in their original form (as v-prefixed strings), properly sorted.


The v prefix in your version numbers prevents their conversion to [System.Version] instances; simply remove it first (not from the input itself; only temporarily, for the purpose of creating a version-info object e.g., v1.1.0 -> 1.1.0).

Additionally, your command can be simplified:

# $tags is an array of lines, as output by `git tag`
$tags | Sort-Object { [version] $_.Substring(1) }
  • [version] is a type accelerator (shorter name) built into PowerShell that refers to [System.Version].[1]

  • You can simply cast a string to [version], which is both more concise and faster than using New-Object.

  • Sort-Object accepts an expression, via a script block ({ ... }), in lieu of a fixed property to sort by; inside the script block, $_ refers to a given input object; $_.Substring(1) simply removes the first character (the v).

    • Note that the expression is used only transiently, for the purpose of sorting; Sort-Object still outputs the original strings - sorted.

With your sample input the above yields (note how v2.10.0 correctly sorts after v2.9.0, which wouldn't be the case with lexical sorting):

v1.1.0
v1.2.0
v1.3.0
v1.4.0
v1.5.0
v1.7.0
v1.8.0
v1.9.0
v2.0.0
v2.1.0
v2.2.0
v2.3.0
v2.4.0
v2.5.0
v2.6.0
v2.7.0
v2.8.0
v2.9.0
v2.10.0
v2.11.0
v2.12.0

If you'd rather output System.Version instances instead of the input strings, the command becomes even simpler (PSv3+ syntax):

[version[]] $tags.Substring(1) | Sort-Object

If there's a possibility that not all strings contained in $tags can be converted this way (due to not having v<version> format), use the following (PSv4+ syntax):

# Reports non-convertible lines as non-terminating errors, but processes all others.
$tags.ForEach({ [version] $_.Substring(1) }) | Sort-Object

This approach ensures that encountering strings that cannot be converted do not break the overall command:

  • Those that can be converted are, and are output.

  • Those that cannot be converted will cause an error that prints to the console and will also be reflected in the automatic $Error collection afterwards. You can suppress console output with 2>$null.


[1] Generally, PowerShell allows you to omit the System. prefix in type names.

mklement0
  • 382,024
  • 64
  • 607
  • 775
3

A more general way to sort numbers of any length contained in strings is Roman Kuzmin's $ToNatural

If you store this in your script/profile:

$ToNatural = { [regex]::Replace($_, '\d+', { $args[0].Value.PadLeft(20) }) }

You can simply use:

$tags | Sort-Object $ToNatural

v1.1.0
v1.2.0
v1.3.0
...
v2.7.0
v2.8.0
v2.9.0
v2.10.0
v2.11.0
v2.12.0
0

I believe you need to parse these strings properly as version objects if you want to sort them without a problem.


$tags = @(
    'v1.1.0'
    'v1.2.0'
    'v1.3.0'
    'v1.4.0'
    'v1.5.0'
    'v1.7.0'
    'v1.8.0'
    'v1.9.0'
    'v2.0.0'
    'v2.1.0'
    'v2.10.0'
    'v2.11.0'
    'v2.12.0'
    'v2.2.0'
    'v2.3.0'
    'v2.4.0'
    'v2.5.0'
    'v2.6.0'
    'v2.7.0'
    'v2.8.0'
    'v2.9.0'
)


 #----------------------------------------------------------------------------# 
 #     Parse Will Fail As 'v' In String is Not Valid Semantic Versioning      # 
 #----------------------------------------------------------------------------# 
$tags | % { 
    $tag = $_
    $version = [version]::new()
    if ([version]::TryParse($tag, [ref]$version))
    {
        $version
    }
    else 
    {"ParseFailed--$($tag)"}

} | Sort-Object


 #----------------------------------------------------------------------------# 
 #                        Parsing String Successfully                         # 
 #----------------------------------------------------------------------------# 
$tags | % { 
    $tag = $_ -replace '[a-zA-Z]'
    $version = [version]::new()
    if ([version]::TryParse($tag, [ref]$version))
    {
        $version
    }
    else 
    {"ParseFailed--$($tag)"}

} | Sort-Object


You could to a ToString() on the object returned if you want to use it as a 2.1.0 type of string as well.

sheldonhull
  • 1,807
  • 2
  • 26
  • 44
  • 1
    While your solution works, it's a lot of ceremony for what can be achieved PowerShell-idiomatically with `[version[]] $tags.Substring(1) | Sort-Object`. – mklement0 Mar 07 '19 at 17:01
  • I was focusing on tryparse as a better way to handle failures, but I get that it's not as short. If the input can be guaranteed to be what he showed I agree it could be done much more succinctly – sheldonhull Mar 07 '19 at 21:53
  • I see - it might help if you added this comment at the top of your answer (that your answer is intentionally more verbose in order to provide error handling). As an aside, a quibble: `System.Version` doesn't implement _semantic_ version numbers (PowerShell _Core_ now has a separate `[semver]` type). – mklement0 Mar 07 '19 at 22:30
  • P.S.: `$tags.ForEach({ [version] $_.Substring(1) }) | Sort-Object` would convert all convertible inputs while reporting non-terminating errors for the others (which print to the console and can also be accessed later via the automatic `$Error` variable). – mklement0 Mar 07 '19 at 23:07
  • 1
    Hey that's great to know. Didn't realize that version was different. Cheers! – sheldonhull Mar 08 '19 at 00:06
0

The problem is that 'v' is not allowed in the version string. You can sort properly if you remove it first:

$tags | Sort-Object {[Version]($_ -replace "[a-zA-Z]","")}

Note that this isn't really removing the 'v' (it creates a copy without it) and it will still be there in the output:

v1.1.0
v1.2.0
v1.3.0
v1.4.0
v1.5.0
...
boxdog
  • 7,894
  • 2
  • 18
  • 27