Search code examples
powershellselect-objectcalculated-property

How to dynamically build Calculated Properties with Select-Object?


I query a REST API which returns a JSON with many subproperties that I need to extract using a calculated property hashtable with Select-Object. The subproperty has always the same name (result), and I want to use a filter or a function to avoid rewriting the hashtable all along.

Example:

some json | ConvertFrom-Json | Select-Object @{ Name="name"; Expression={ $_."name".result}}, @{ Name="year"; Expression={ $_."year".result}}, etc.

I thought it will be more convenient (and easy) to build a filter that would return the hashtable:

filter Get-Result {
     param (
         [string]$field
     )
     @{N=$field; E={ $_."$field".result}}
}

But $field is not replaced in Expression block. I don't understand why

> Get-Result -field "year"

Name                           Value
----                           -----
N                              year
E                               $_."$field".result

How to make $_."$field".result be $_."year".result in Expression block?


Solution

  • Two options: closure or source code generation

    1. Create a closure

    You can call GetNewClosure() on the scriptblock passed to the Expression entry - this will cause PowerShell's internal compiler to create a dynamic module that "remembers" the value bound to $fieldName (or any other variable references to the scope where GetNewClosure() is called):

    # create some dummy objects for testing
    $objects = 1..10|%{
      [pscustomobject]@{
        name = @{ result = "Name_${_}" }
        year = @{ result = Get-Random -Minimum 1900 -Maximum 2025 }
      }
    }
    
    # define filter to generate property expression tables
    filter Get-Result {
       # to take advantage of pipeline input, use `$_`
       $fieldName = "$_"
       # return property table with closure attached
       @{N=$fieldName; E={ $_."$fieldName".result}.GetNewClosure()}
    }
    
    # this now works as expected
    $objects |Select-Object -Property ('name','year' |Get-Result)
    

    2. Construct scriptblock from source code

    The alternative is to construct the expression block by generating its source code and then passing that off to [scriptblock]::Create():

    filter Get-Result {
       # to take advantage of pipeline input, use `$_`
       $fieldName = "$_"
    
       # escape any literal `'`s
       $escapedName = [System.Management.Automation.Language.CodeGeneration]::EscapeSingleQuotedStringContent($fieldName)
       $expressionText = '$_.''{0}''.result' -f $escapedName
       $expression = [scriptblock]::Create($expressionText)
    
       # return property table with closure attached
       @{N=$fieldName; E=$expression}
    }