Simplified syntax for calculated Properties with Select-Object

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):

Get-ChildItem | Select-Object Name, CreationTime,  @{Name="Kbytes";Expression={$_.Length / 1Kb}}
Get-ChildItem | Select-Object Name, @{Name="Age";Expression={ (((Get-Date) - $_.CreationTime).Days) }}

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 2 Example
[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:

Get-ChildItem | Select-Object Name, CreationTime,  @{n="Kbytes";e={$_.Length / 1Kb}}
Get-ChildItem | Select-Object Name, @{n="Age";e={ (((Get-Date) - $_.CreationTime).Days) }}

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:

Get-ChildItem | Select-Object Name, CreationTime,  @{Kbytes={$_.Length / 1Kb}}
Get-ChildItem | Select-Object Name, @{Age={ (((Get-Date) - $_.CreationTime).Days) }}

Let’s see how this could be implemented with a little test function:


function mySelect {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline=$true)]
[psobject]$InputObject,
[Parameter(Position=0)]
$Property
)
begin{
#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
}
}
process{
Select-Object @PSBoundParameters
}
}

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:

$Metadata = New-Object System.Management.Automation.CommandMetaData (Get-Command Select-Object)
$proxyCmd = [System.Management.Automation.ProxyCommand]::Create($Metadata) | clip

Below is the code of the full proxy command highlighting the modified lines (as compared to the scaffold code):

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
        }
    }

}

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

4 thoughts on “Simplified syntax for calculated Properties with Select-Object

  1. Correct me if I’m wrong, but it looks like this modified Select-Object doesn’t support native calculated properties: @{n=’foo’, e=’bar’} , so it’s not backward compatible ? Anyway, great idea, this should have been implemented by PS team years ago, like simplified Where-Object syntax.

    Like

    1. You are right, the way I implemented the proxy command it basically just overwrites the syntax for calculated properties. I should probably log this on Connect, it might make it into the product if enough people vote for it.

      Like

  2. How do you come up with these ideas? This is pretty impressive!!! Get-ChildItem | Select-Object Name, CreationTime, @{Kbytes={$_.Length / 1Kb};MBytes={$_.length / 1Mb}} Works!

    How do I learn more about proxycmd? I’ve been looking into AST… Feels like the chicken egg situation.. I have a solution looking for a problem. Can’t wait for the next post!

    Rg./Irwin

    Liked by 1 person

I'd love to hear what you think