Search code examples
arrayspowershellobjectsubset

How to access the elements of an array nested inside a PSCustomObject?


The following object is a toy example to illustrate my question:

$food = @(
    [PSCustomObject]@{Category = "Fruits"; Items = @("Mango", "Pineapple", "Orange", "Tangerine")},
    [PSCustomObject]@{Category = "Cities"; Items = @("Abidjan", "Douala")},
    [PSCustomObject]@{Category = "Animals"; Items = @("Dog", "Bird", "Cat")}
)

$food

Category Items
-------- -----
Fruits   {Mango, Pineapple, Orange, Tangerine}
Cities   {Abidjan, Douala}
Animals  {Dog, Bird, Cat}

I would like to access all items of the "Fruits" category, for example. I tried to first determine the index of "Fruits" in the Category property then use it in the "Items" property. I thought that the entire array of fruit names would be returned; however, it did not work as expected:

$i = $food.Category.IndexOf("Fruits")
$food[$i]
$food.Items[$i]

Mango

How should I go about it?

Thank you.


Solution

  • You need to apply index $i to the $food array itself, not to .Items:

    $food[$i].Items # -> @('Mango', 'Pineapple', 'Orange', 'Tangerine')
    

    However, a more PowerShell-idiomatic solution, using the Where-Object cmdlet to perform the category filtering (with simplified syntax), would be:

    ($food | Where-Object Category -eq Fruits).Items
    

    Note:

    • The above could potentially return multiple elements from the $food array, namely if more than one element has a .Category property value 'Fruits'.

    • To limit output to the first element, you could append a Select-Object -First 1 pipeline segment, but there's a simpler alternative: the intrinsic .Where() method has an overload that allows you to stop at the first match:[1]

      $food.Where({ $_.Category -eq 'Fruits' }, 'First').Item
      
      • It is somewhat unfortunate that the Where-Object cmdlet doesn't have an analogous feature; GitHub issue #13834 suggests enhancing the Where-Object cmdlet to ensure feature parity with its method counterpart, .Where().

    As for what you tried:

    $food.Items[$i]
    

    Accessing .Items on the array stored in $food performs member-access enumeration.

    That is, the .Items property on each object stored in the array is accessed, and the resulting values are returned as a single, flat array of all such property values.

    • To spell it out: $food.Items returns the following array:

      @('Mango', 'Pineapple', 'Orange', 'Tangerine', 'Abidjan', 'Douala', 'Dog', 'Bird', 'Cat')
      
    • As an aside: Your index-finding code, $food.Category.IndexOf("Fruits"), also makes use of member-access enumeration, relying on returning the .Category property values across all elements of the $food array.

    Indexing into the resulting array (with [$i], with $i being 0) then only extracts its first element, i.e. 'Mango'.


    [1] Using the .Where() method has additional implications: (a) the input object(s) must be collected in full, up front, and (b) what is output is invariably a collection, namely of type [System.Collections.ObjectModel.Collection[psobject]] - see this answer for details.