Ted Shaneyfelt
Ted Shaneyfelt

Reputation: 903

function to simplify adding custom property to powershell select

Powershell can add custom properties with somewhat messy syntax, as described in the about_Calculated_Properties help topic; e.g.:

Get-Date | Select-Object Year, @{name='foo'; expression={123}}

Year foo
---- ---
2024 123

I want to clean it up a bit, so I can write something more like this:

Get-Date | Select-Object Year, (prop 'foo' 123)

How can I implement a prop function that enables this simplified syntax?

Upvotes: -6

Views: 590

Answers (5)

mklement0
mklement0

Reputation: 439812

  • Santiago's helpful answer shows an alternative to what you're asking for, by overriding the built-in Select-Object cmdlet:

    • It allows simplifying the original syntax, e.g. @{ name='foo'; expression={123} }, to the - still hashtable literal-based - form @{ foo = { 123 } }, but it also limits you to only using this simplified syntax; that is, the original calculated-property syntax is then no longer supported.
      That said, the implementation could be refined to default to the official syntax and fall back to the simplified one, if the mandatory entries aren't present.
  • Your (first) own helpful answer mostly addresses your question as asked, except for the - syntactically still awkward - need to enclose a static property value in { ... }, i.e. in a script block; that is, you asked for (prop 'foo' 123), but the prop implementation in your answer requires (prop 'foo' { 123 }) instead.

  • Fully implementing what your question asks for is possible, assuming that you're willing to forgo the normal (albeit rarely seen in practice) shorthand syntax of providing a property name of the input objects as a string, e.g. 'foo' instead of the verbose equivalent of using a script block in which the property is explicitly accessed on the input object at hand, via the automatic $_ variable, { $_.foo }

function prop {
  param([Parameter(Mandatory)] $name, [Parameter(Mandatory)] $val)

  if ($val -isnot [scriptblock]) {
    # Wrap the original value in a script block and
    # use a closure to capture it, so it is also available when
    # executed in the caller's scope.
    $val = { $val }.GetNewClosure()
  }
  # Otherwise: If already a script block, use as-is.

  # Create and output a hashtable using the prescribed format
  # for serving as a calculated property.
  @{ name = $name; expression = $val }
}

The above lets you pass literal values as-is, while also supporting script block-based values; e.g.:

Get-Date |
  Select-Object Year, 
                (prop foo 123),             # Static value
                (prop MyDay { $_.Day + 1 }) # Value based on input object

Sample output:

Year foo MyDay
---- --- -----
2024 123    19

Upvotes: 0

Ted Shaneyfelt
Ted Shaneyfelt

Reputation: 903

An alternative to the -NotePropertyMembers approach in y y's answer is the combination of
-NotePropertyName and -NotePropertyValue

Get-Date | 
Add-Member -Force -PassThru -NotePropertyName foo -NotePropertyValue bar | 
Select-Object Year, Foo

or simply

date | Add-Member foo bar -Force -PassThru | select Year, Foo

The same output in either case

Year foo
---- ---
2024 123

-Force may be required to avoid name collision errors even if the name isn't present in the original object

Upvotes: 0

y y
y y

Reputation: 349

Get-Date | 
Add-Member -NotePropertyMembers @{ 'foo' = 123 }  -PassThru |
Select-Object Year, foo

Upvotes: 0

Ted Shaneyfelt
Ted Shaneyfelt

Reputation: 903

Define the function as follows

function prop($name,$val) {@{name=$name; expression=$val}}

It gives the expected results below. The curly braces are not optional.

Get-Date | Select-Object Year, (prop 'foo' {123})

Year foo
---- ---
2024 123

Upvotes: 0

Santiago Squarzon
Santiago Squarzon

Reputation: 60838

An alternative to using a function to create calculated properties for you is to proxy the cmdlet itself to handle this use case. The proxy command is created via ProxyCommand.Create Method:

[System.Management.Automation.ProxyCommand]::Create((Get-Command Select-Object))

And from there you can add additional logic to it, as an example to handle the use where Select-Object could accept @{ Key = Value } and transform it to @{ N=Key; E=Value }, the proxy could look something like this (I'm reducing it a lot from the original output however this cmdlet's param block is very extensive. The relevant code is in the begin block):

function Select-Object {
    [CmdletBinding(
        DefaultParameterSetName = 'DefaultParameter',
        HelpUri = 'https://go.microsoft.com/fwlink/?LinkID=2096716',
        RemotingCapability = 'None')]
    param(
        [Parameter(ValueFromPipeline = $true)]
        [psobject]
        ${InputObject},

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

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

        [Parameter(ParameterSetName = 'SkipLastParameter')]
        [Parameter(ParameterSetName = 'DefaultParameter')]
        [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 = 'DefaultParameter')]
        [Parameter(ParameterSetName = 'IndexParameter')]
        [switch]
        ${Wait},

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

    begin {
        # if the function was called with `-Property`
        if ($PSBoundParameters.ContainsKey('Property')) {
            # reconstruct from @{ Key = Value } to @{ N=Key; E=Value }
            $PSBoundParameters['Property'] = foreach ($prop in $Property) {
                if ($prop -isnot [hashtable]) {
                    # nothing to do here,
                    # just output the value as-is and go next
                    $prop
                    continue
                }

                # here we assume the property is calculated
                foreach ($keyvalue in $prop.GetEnumerator()) {
                    @{ Name = $keyvalue.Key; Expression = $keyvalue.Value }
                }
            }
        }

        $steppablePipeline = { Microsoft.PowerShell.Utility\Select-Object @PSBoundParameters }.
            GetSteppablePipeline($MyInvocation.CommandOrigin)
        $steppablePipeline.Begin($PSCmdlet)
    }

    process {
        $steppablePipeline.Process($InputObject)
    }

    end {
        $steppablePipeline.End()
    }
}

Then using the proxy function, this syntax would work just fine:

Get-Date | Select-Object Year, @{ Month = { $_.Month }}, @{ Day = 'Day' }

Also this syntax becomes valid:

Get-Date | Select-Object @{
    New      = 'Month'
    Property = { $_.Day }
}

Upvotes: 3

Related Questions