causa prima
causa prima

Reputation: 1712

Reusable sort-function in PowerShell

I have a list of strings containing the names of scripts. I need to check the versions of these scripts in order to figure out whether they have a version lower than the latest executed version and to only execute those scripts with a version number higher than the latest version, in order of the script sequence for that version. I also have a list of strings for the already executed scripts, so in order to determine the latest script, that list needs to be sorted.

Thus, I wrote some code to parse the version details from these strings and wanted to sort both lists with the same "function" - but I can't get the sorting to work. The Sort-Object part works when I call it directly, but not with the function Sort-Scripts. I am new to PowerShell, so I must be overlooking something fundamental. But I can't figure out what.

function Get-SemVer($version) {
    $version -match '^(?<major>\d+)(\.(?<minor>\d+))?(\.(?<patch>\d+))?(\-(?<pre>[0-9A-Za-z\-\.]+))?(\+(?<build>[0-9A-Za-z\-\.]+))?$' | Out-Null
    if ($matches) {
        [PSCustomObject]@{
            Major = [int]$matches['major']
            Minor = [int]$matches['minor']
            Patch = [int]$matches['patch']
            PreReleaseLabel = [string]$matches['pre']
            BuildLabel = [string]$matches['build']
        }
    }
}

function ParseVersionDetails {
    param([string]$InputString)

    if ($InputString -match '^(?<version>.*?)_(?<sequence>\d+)_?(?<scriptname>.*)') {
        [PSCustomObject]@{
            Version = Get-SemVer $matches['version']
            Sequence = [int]$matches['sequence']
            ScriptName = $matches['scriptname']
            FullName = $InputString
        }
    }
}

function Sort-Scripts {
    process {
        @($Input) | Sort-Object -Property @{Expression={$_.Version.Major}}, @{Expression={$_.Version.Minor}}, @{Expression={$_.Version.Patch}}, @{Expression={$_.Version.PreReleaseLabel}}, @{Expression={$_.Version.BuildLabel}}, @{Expression={$_.Sequence}}
    }
}

$list = @(
    "7.9.0-dev-IRIS-Dyn_02_SomeScript",
    "7.9.0-dev-IRIS-Dyn_01_SomeScript",
    "7.9.1-prod-IRIS-Dyn+13_01_OtherScript",
    "7.8.5_02_AnotherScript",
    "7.8.5_01_AnotherScript"
)

"#### Doesn't work"
$list | ForEach-Object { ParseVersionDetails -InputString $_ } | Sort-Scripts

"####  Works"
$list | ForEach-Object { ParseVersionDetails -InputString $_ } | Sort-Object -Property @{Expression={$_.Version.Major}}, @{Expression={$_.Version.Minor}}, @{Expression={$_.Version.Patch}}, @{Expression={$_.Version.PreReleaseLabel}}, @{Expression={$_.Version.BuildLabel}}, @{Expression={$_.Sequence}}

"Done"

Upvotes: 1

Views: 61

Answers (3)

mklement0
mklement0

Reputation: 439777

The Sort-Object part works when I call it directly, but not with the function Sort-Scripts

You must pass all pipeline input to a single Sort-Object call, so do not use a process block:

function Sort-Scripts {
  # *No* process block; no need for @(...)
  $input | Sort-Object -Property { $_.Version.Major }, { $_.Version.Minor }, { $_.Version.Patch }, { $_.Version.PreReleaseLabel }, { $_.Version.BuildLabel }, Sequence
}

Note:

  • Not using a process block makes the function body run once, after all pipeline input has been received and collected in $input (see below); that is, it runs implicitly as if it were enclosed in an end block.

    • Note that collecting all pipeline input first can be problematic with large input sets or when true streaming processing is required, but that isn't a problem in your use case, given that Sort-Object of technical necessity too must collect all its input first.

    • If streaming processing is required, you'll have to write a so-called proxy function that uses a steppable pipeline.

  • In cases where you do need a process block, it is better to use the automatic $_ variable rather than the automatic $input variable; the latter is an enumerator and meant to be used to represent the collected-up-front pipeline input.

  • Also, as Mathias notes, and as reflected above, you don't need full-fledged calculated properties (e.g. @{ Expression={ $_.Version.Major } }) to pass dynamic sort criteria to Sort-Object - a script block alone will do (e.g. { $_.Version.Major }).

Upvotes: 3

JosefZ
JosefZ

Reputation: 30183

Well-documented in about_automatic_variables:

$input

Contains an enumerator that enumerates all input that's passed to a function. The $input variable is available only to functions, script blocks (which are unnamed functions), and script files (which are saved script blocks).

  • In a function without a begin, process, or end block, the $input variable enumerates the collection of all input to the function.

  • In the begin block, the $input variable contains no data.

  • In the process block, the $input variable contains the current object in the pipeline.

  • In the end block, the $input variable enumerates the collection of all input to the function.

Follow sirtao's comment: just remove the process{} block from Sort-Scripts, or replace it with end{}.

Upvotes: 0

sirtao
sirtao

Reputation: 2880

Powershell has a Type for Semantic Versioning: [semver]
(and [version] for regular(?) versioning)

And you do not need a whole function to sort, you can just prepare a splat with the parameters for Sort-Object

# Evaluate a name with Approved Verbs
function ParseVersionDetails {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline, Mandatory)]
        [string]$InputString
    )
    process {
        # Check if there was a match
        if ($InputString -match '^(?<version>.*?)_(?<sequence>\d+)_?(?<scriptname>.*)') {
            
            # will valorize $version with $false of the parsing fails.   
            # otherwise it will valorize it with the parsed [semver] version number.  
            # [ref] requires the variable being created in advance, no matter if with just $null
            $version = $null
            # suppressing the boolean output.   
            $null = [semver]::TryParse($matches['version'], [ref]$version)

            [PSCustomObject]@{
                Version    = $version
                Sequence   = [int]$matches['sequence']
                ScriptName = $matches['scriptname']
                FullName   = $InputString
            }
        }
    }
}

# Splat ariable for Sort-Object .  
$SortParameters = @{ 
    Property = @{Expression = { $_.Version.Major } },
    @{Expression = { $_.Version.Minor } }, 
    @{Expression = { $_.Version.Patch } }, 
    @{Expression = { $_.Version.PreReleaseLabel } }, 
    @{Expression = { $_.Version.BuildLabel } }, 
    @{Expression = { $_.Sequence } }
}

$list = @(
    '7.9.0-dev-IRIS-Dyn_02_SomeScript',
    '7.9.0-dev-IRIS-Dyn_01_SomeScript',
    '7.9.1-prod-IRIS-Dyn+13_01_OtherScript',
    '7.8.5_02_AnotherScript',
    '7.8.5_01_AnotherScript',
    'Made.to.fail'
)

$list | ParseVersionDetails | Sort-Object @SortParameters 


'Done'

Upvotes: 0

Related Questions