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 also accepts abbreviated key names, 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:

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
        }
    }
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Select-Object
.ForwardHelpCategory Cmdlet
#>
}

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

Advertisements

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

    • 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

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s