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:
[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?