1

I'm creating an array of PSObjects with calculated properties. I need one property that is calculated based on another property of the same object. How do I do that? Example - let's say I have array of strings like "a_1", "b_2", "c_3" etc. and I have a lookup function that returns something based on the first part of those strings, i.e. someLookUpFunction('a') would return "AA" with input of "a". Now I need a property in my object that has this calculated 'AA' based on the my 'name' property

$stringArray = @('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object{
  New-Object PSObject -Property @{
     'name' = ($_ -split "_")[0]
     'extendedName' = {$name = ($_ -split "_")[0]; someLookUpFunction($name) }
  }
}

The code above doesn't work in part that the output for 'extendedName' property is just this script block. How do I make it to take the value?

miguello
  • 544
  • 5
  • 15
  • 1
    You can use sub-expression operator: `'extendedName' = $($name = ($_ -split "_")[0]; someLookUpFunction $name)`. You could just use `$name = ($_ -split "_")[0]` before the `new-object` command. Then reference `$name` inside. – AdminOfThings Dec 09 '20 at 18:28
  • 1
    As an aside: PowerShell functions, cmdlets, scripts, and external programs must be invoked _like shell commands_ - `foo arg1 arg2` - _not_ like C# methods - ~~`foo('arg1', 'arg2')`. If you use `,` to separate arguments, you'll construct an _array_ that a function sees as a _single argument_. To prevent accidental use of method syntax, use [`Set-StrictMode -Version 2`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/set-strictmode) or higher, but note its other effects. See [this answer](https://stackoverflow.com/a/65208621/45375) for more information. – mklement0 Dec 09 '20 at 18:44

2 Answers2

3

If you need to capture the output of an expression within an expression, you can use the sub-expression operator $().

$stringArray = @('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
  [pscustomobject]@{
     'name' = ($_ -split "_")[0]
     # You can't reference the name property above in this property because it has not been created yet.
     'extendedName' = $($name = ($_ -split "_")[0]; someLookUpFunction $name)
  }
} 

However, that should not be necessary in your example. You can define a variable before the custom object creation and then reference it within the object creation code:

$stringArray = @('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
  $name = ($_ -split '_')[0]
  [pscustomobject]@{
     'name' = $name
     'extendedName' = someLookUpFunction $name
  }
} 

You could also pass expressions to parameters directly provided it can be tokenized correctly:

$stringArray = @('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
  [pscustomobject]@{
     'name' = ($_ -split '_')[0]
     'extendedName' = someLookUpFunction ($_ -split '_')[0]
  }
} 

Note: The proper way to call a function without using the pipeline is functionName -parametername parametervalue or functionName parametervalue if positional parameters are enabled. The syntax functionName(parametervalue) could have unintended consequences. See this answer for a deeper dive into function/method calling syntax.

You cannot access the name property of an object before that object has been created.

AdminOfThings
  • 23,946
  • 4
  • 17
  • 27
  • Awesome, thanks a lot! Was not familiar with sub-expression operator - this is why my code wasn't working. – miguello Dec 09 '20 at 19:21
3

In addition to AdminOfThings Good Answer you can bypass the loop altogether using a select statement with the calculated property hash syntax:

$stringArray = @('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | 
Select-Object @{Name = 'Name'; Expression = { ($_ -Split '_')[0] } },
    @{Name = 'ExtendedName'; Expression = { SomeLookupFunction ($_ -Split '_')[0] } }

For the efficiency of not executing -Split '_' 2x, if you do go with a loop just use a variable to and reference twice.

Altered version of AdminOfThings Example:

$stringArray = @('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | ForEach-Object {
  $TmpName = ($_ -split '_')[0]
    [pscustomobject]@{
     'name' = $TmpName
     'extendedName' = someLookUpFunction $TmpName
  }
} 

It's also correct that you can't reference a property before it's been added to an object. One way around this is to just use 2 select statements:

$stringArray = @('a_1', 'b_2', 'c_3')
$objectArray = $stringArray | 
Select-Object @{Name = 'Name'; Expression = { ($_ -Split '_')[0] } } |
Select-Object *, @{Name = 'ExtendedName'; Expression = { SomeLookupFunction ($_ -Split '_')[0] } }

This may have some readability advantage, but, I try to avoid it in favor of invoking as few commands as possible.

Steven
  • 6,817
  • 1
  • 14
  • 14