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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?
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.
LikeLike
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.
LikeLike
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
LikeLiked by 1 person
Hey Irwin,
I don’t know I just love tweaking things ;-).
There are plenty resources about proxy commands, I like what Shay Levy and Kirk Munro have come up with http://blogs.microsoft.co.il/scriptfanatic/2012/03/30/leveraging-proxy-functions-in-powershell/.
Dirk
LikeLiked by 1 person