Summary
Sorting version numbers as strings can fail to order them correctly, but the
[Version]
type accelerator can be used to sort them correctly.
Introduction
Here at Model Technology Solutions, we spend a lot of our time writing PowerShell and we have a number of processes for managing that code. As part of one of our internal tools, we keep a repository of PowerShell modules containing multiple versions of any given module. Each version of a given module is stored in a separate folder, the name of which matches the respective version, with all version folders inside a parent folder for the module, the name of which matches the module name.
Recently I had need to programmatically find the most recent version of each of the modules contained in our repository and import them into a shell session. Naturally, the first instinct was to use PowerShell’s
Sort-Object
cmdlet to sort by the version-named folders and select the first one. Unfortunately in this instance, sorting versions as strings gave incorrect results – “1.10.0” is seen as a lower version than “1.2.0”. Therefore, I had to come up with a better way of sorting.
This post contains a slew of PowerShell code which can be used to illustrate and solve the issue at hand. Follow along as we walk through the code, the issue, and the solution, then feel free to take the code and adapt it as needed to solve issues you face.
Setting Up the Demo
In the scenario this post will follow, the goal is to programmatically find the most recent .psd1 file for a given PowerShell module within a folder structure where the parent folder’s name matches the module’s (and thus the .psd1 file’s) name, with child folders named after the contained version of the module (“1.0.0”, “1.1.0”, “1.9.0”, “1.10.0”, etc.) and containing the files for that version. The core problem to solve is determining a method of sorting the version numbers in order to accurately place them in order and select the most recent version available. While this scenario is specific to PowerShell modules, the core problem and the method of solving it can be applied to any scenario in which you need to sort version numbers.
In order to follow along with this post’s demo, you’ll need to have a folder containing multiple versions of a PowerShell module with the contents arranged by version. Fortunately, that setup is easy to replicate anywhere you have the PowerShellGet module installed. (If you’re running Windows 10 or Server 2016, you already have it.) Execute the following code to download all current versions of the AzureRm.Profile module from the PowerShellGallery to your local computer. The root folder for the AzureRm.Profile module will then be saved to another variable for use in the next section. Here is the code:
# Define the location where modules are/will be stored [string]$psModulesRootFolder = "C:\Temp\Demo-PSModules" # Display all versions of the AzureRm.Profile module available on the repository # Note - at the time of writing, the highest available version on the PSGallery is v2.2.0 Find-Module -Name AzureRm.Profile -AllVersions -Repository PSGallery # Download all versions of the AzureRm.Profile module to the local module folder # Note - the foreach-object loop is required, otherwise only the most recent version will be downloaded Find-Module -Name AzureRm.Profile -AllVersions -Repository PSGallery | %{Save-Module -InputObject $_ -Path $psModulesRootFolder -Force} # Load the list of modules available in the folder $psModuleFolders = Get-ChildItem -Path $psModulesRootFolder -Directory # Isolate the first module for the demo $psModuleFolder = $psModuleFolders | Select-Object -First 1
Note that, at the time of this writing, the highest available version of the AzureRm.Profile module on the PSGallery is version 2.2.0. If newer versions have come out since this post was originally written, just delete anything above 2.2.0 in order to accurately follow along. The solution code doesn’t depend on this, but the illustration code might.
Illustrating the Issue
Now that we’re all set up, we can illustrate the issue with sorting by the version numbers directly. The following code will find all .psd1 files in all of the version folders and then sort them by the file path, writing that out to the shell for comparison. Execute the code and look at the results.
# Find all of the .psd1 files in the module folder $moduleFiles = Get-ChildItem -Path $psModuleFolder.FullName -Filter "*.psd1" -Recurse # Sort by file path with the "newest" at the top $moduleFiles | Sort-Object -Property FullName -Descending | Select-Object -ExpandProperty FullName # Notice that in this sorting, v1.0.10 is seen as a LOWER version than v1.0.2.
Here are the results from that last command:
If you look at the list, you’ll see that it’s actually sorting v1.0.10 as a lower version than v1.0.2. Yet, technically, since the highest version we have in our folder right now is v2.2.0 and there aren’t any double-digit versions higher, if we were to select the first object now, this sort would work for now. As soon as a newer version breaching double-digits was released, though, we’d run into an issue.
To better illustrate the problem, let’s create a fake version 2.10.0 and see how it gets sorted. The following code will duplicate the v2.2.0 folder, changing the version name to 2.10.0 in the process. It will then find the .psd1 files, sort them, and output the filenames, just as before:
# To better illustrate this, let's make a version 2.10.0, which SHOULD be seen as the highest version $demoFolder = $psModuleFolder | Get-ChildItem -Directory | ?{$_.Name -eq '2.2.0'} $demoFolder | Copy-Item -Destination ((Split-Path -Path $demoFolder.FullName -Parent) + '\2.10.0') -Recurse # Find all the .psd1 files again and sort by file path with the "newest" at the top $moduleFiles = Get-ChildItem -Path $psModuleFolder.FullName -Filter "*.psd1" -Recurse $moduleFiles | Sort-Object -Property FullName -Descending | Select-Object -ExpandProperty FullName # v2.2.0 is at the top of the list, while it should be v2.10.0
Here are the results from that command:
Looking at that list, it’s showing v2.2.0 as a higher version than v2.10.0. If we were to simply select the first object in our sorted list, we’d have the wrong one. Execute this code to see that:
# Select the "most recent version", sorting by file path $moduleFile = $moduleFiles | Sort-Object -Property FullName -Descending | Select-Object -First 1 # Write the file name to the host to show which was selected $moduleFile.FullName # v2.2.0 is selected despite not being the most recent version
And the results:
Clearly,
Sort-Object
alone is not going to cut it in this scenario if we want to guarantee the accuracy of our results. Fortunately, the solution is fairly straightforward to implement.
Sorting by Version Successfully
The problem can be summarized as this – sorting version numbers as strings fails because the version numbers lack the consistent character counts (specifically, the leading 0s) that would be needed for them to be sorted properly as strings. The solution is fortunately very simple – don’t treat them as strings. Since we’re working with version numbers, let’s treat them as version numbers and let .NET take care of the work for us.
PowerShell is a .NET-based language and exposes the full breadth of the .NET Framework to us scripters. We can leverage the
[System.Version]
class, or its
[Version]
type accelerator, to process the version numbers and sort them accurately.
In order to do this successfully, we need to extract the version number from the rest of the file path before performing the sort and selecting the most recent version. Once we have that information, we can explicitly select the .psd1 file contained in the most recent version folder. Being PowerShell, this can be done a number of ways, but in my example, I’m making heavy use of the PowerShell pipeline to perform the extraction of version numbers as sorting, then a second command to match the desired module file. Execute the following code to perform that work and select the desired module file, then write out the module’s file path to the host to confirm the results:
# Identify the most recent version available $mostRecent = ($moduleFiles | Select-Object -ExpandProperty FullName | Split-Path -Parent | Split-Path -Leaf | %{[version]$_} | Sort-Object -Descending | Select-Object -First 1).ToString() # Grab the most recent version file $moduleFile = $moduleFiles | ?{ ($_ | Select-Object -ExpandProperty FullName | Split-Path -Parent | Split-Path -Leaf) -eq $mostRecent } # Write the file name to the host to show which was selected $moduleFile.FullName
And the results:
We have success – version 2.10.0 was successfully identified and returned to the shell!
As you can see on line 73 in the previous code block, once we’ve extracted the version number from the file path (using a pair of
Split-Path
commands), we’re casting the resulting string as a .NET Version class which is then sent back to the pipeline and sorted before being re-converted back to a string. We’re essentially performing the same sort as above, but by casting the data into the Version class first, PowerShell is able to accurately recognize what it is we are trying to sort and give us the desired output.
Conclusion
The goal of this post was to present an issue found when sorting version numbers as strings and present a solution to that issue, all with code which can be used to follow along at home. My hope is that this has illustrated the power and flexibility of PowerShell as well as one of the ways .NET can be used to solve issues we come across in our daily tasks. The full script I’ve used throughout this post will be linked at the end of the post; feel free to grab it and modify the code as needed to solve your own issues. Good luck, and happy scripting!