2 min read

Categories

Tags

3798238251_59749f23cb_m

Following on my journey in an attempt to make PowerShell work exactly the way I would like it to work I had a look into the syntax for calculated properties with Select-Object. Calculated properties for Select-Object are basically syntactic accidents sugar to add custom properties to objects on the fly (examples are taken from here): [code language=”powershell”] Get-ChildItem | Select-Object Name, CreationTime, @{Name=”Kbytes”;Expression={$.Length / 1Kb}} Get-ChildItem | Select-Object Name, @{Name=”Age”;Expression={ (((Get-Date) - $.CreationTime).Days) }} [/code] Looking at the documentation for Select-Object we can see that the syntax for the calculated properties on the Property parameter permits different key names and value type combinations as valid arguments:

[TYPE]KEYNAME 1[TYPE]KEYNAME 2Example
[STRING]Name [STRING]Expression @{Name="Kbytes";Expression="Static value"}
[STRING]Name [SCRIPTBLOCK]Expression @{Name="Kbytes";Expression={$_.Length / 1Kb}}
[STRING]Label [STRING]Expression @{Label="Kbytes";Expression="Static value"}
[STRING]Label [SCRIPTBLOCK]Expression @{Label="Kbytes";Expression={$_.Length / 1Kb}}

Most of the people already familiar with PowerShell also know that the parameter can acceptsabbreviated key names, too. E.g. just using the first letter: [code language=”powershell”] Get-ChildItem | Select-Object Name, CreationTime, @{n=”Kbytes”;e={$.Length / 1Kb}} Get-ChildItem | Select-Object Name, @{n=”Age”;e={ (((Get-Date) - $.CreationTime).Days) }} [/code] What I find confusing about this syntax is the fact that we need two key/value pairs in order to actually provide a name and a value. In my humble opinion it would make more sense if the Property parameter syntax for calculated properties would work the following way: [code language=”powershell”] Get-ChildItem | Select-Object Name, CreationTime, @{Kbytes={$.Length / 1Kb}} Get-ChildItem | Select-Object Name, @{Age={ (((Get-Date) - $.CreationTime).Days) }} [/code] Let’s see how this could be implemented with a little test function: https://gist.github.com/d33f3851cc54e2d5cfbf

Now we can go ahead and create the proxy function to make Select-Object behave the same way. First we will need to retrieve the scaffold for the proxy command. The following will copy the same to the clip board: [code language=”powershell”] $Metadata = New-Object System.Management.Automation.CommandMetaData (Get-Command Select-Object) $proxyCmd = [System.Management.Automation.ProxyCommand]::Create($Metadata) | clip [/code] Below is the code of the full proxy command highlighting the modified lines (as compared to the scaffold code): [code language=”powershell” highlight=”64,65,66,67,68,69,70,71,72,73,74,75,76,77,78”] function Select-Object{ [CmdletBinding(DefaultParameterSetName=’DefaultParameter’, HelpUri=’http://go.microsoft.com/fwlink/?LinkID=113387’, RemotingCapability=’None’)] param( [Parameter(ValueFromPipeline=$true)] [psobject] ${InputObject},

    [Parameter(ParameterSetName='SkipLastParameter', Position=0)]
    [Parameter(ParameterSetName='DefaultParameter', Position=0)]
    [System.Object[]]
    ${Property},

    [Parameter(ParameterSetName='SkipLastParameter')]
    [Parameter(ParameterSetName='DefaultParameter')]
    [string[]]
    ${ExcludeProperty},

    [Parameter(ParameterSetName='DefaultParameter')]
    [Parameter(ParameterSetName='SkipLastParameter')]
    [string]
    ${ExpandProperty},

    [switch]
    ${Unique},

    [Parameter(ParameterSetName='DefaultParameter')]
    [ValidateRange(0, 2147483647)]
    [int]
    ${Last},

    [Parameter(ParameterSetName='DefaultParameter')]
    [ValidateRange(0, 2147483647)]
    [int]
    ${First},

    [Parameter(ParameterSetName='DefaultParameter')]
    [ValidateRange(0, 2147483647)]
    [int]
    ${Skip},

    [Parameter(ParameterSetName='SkipLastParameter')]
    [ValidateRange(0, 2147483647)]
    [int]
    ${SkipLast},

    [Parameter(ParameterSetName='IndexParameter')]
    [Parameter(ParameterSetName='DefaultParameter')]
    [switch]
    ${Wait},

    [Parameter(ParameterSetName='IndexParameter')]
    [ValidateRange(0, 2147483647)]
    [int[]]
    ${Index})

begin
{
    try {
        $outBuffer = $null
        if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
        {
            $PSBoundParameters['OutBuffer'] = 1
        }
        #only if the property array contains a hashtable property
        if ( ($Property | where { $_ -is [System.Collections.Hashtable] }) ) {
            $newProperty = @()
            foreach ($prop in $Property){
                if ($prop -is [System.Collections.Hashtable]){
                    foreach ($htEntry in $prop.GetEnumerator()){
                       $newProperty += @{n=$htEntry.Key;e=$htEntry.Value}
                    }
                }
                else{
                    $newProperty += $prop
                }
            }
            $PSBoundParameters.Property = $newProperty
        }
        $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Select-Object', [System.Management.Automation.CommandTypes]::Cmdlet)
        $scriptCmd = {& $wrappedCmd @PSBoundParameters }
        $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
        $steppablePipeline.Begin($PSCmdlet)
    } catch {
        throw
    }
}

process
{
    try {
        $steppablePipeline.Process($_)
    } catch {
        throw
    }
}

end
{
    try {
        $steppablePipeline.End()
    } catch {
        throw
    }
}

} [/code] Putting the above into your profile (You can read here and here on how to work with profiles) will make the modified Select-Object available in every session. What do you think of the syntax for calculated properties?

shareThoughts


Photo Credit: ChrisK4u via Compfight cc