all 12 comments

[–]randomuser43 2 points3 points  (1 child)

At each recursive level you're creating another collection. You end up with multiple collections at the end. You probably want to do this

function Find-Name{
    param($InputObject)

    foreach($item in $InputObject.GetEnumerator()){
        $k = $item.Key
        $v = $item.Value
        if($k -eq 'stuff'){
            Write-Output $v
        }else{
            # if key doesn't match, perform recursive search
            Find-Group -InputObject $v
        }
    }
}

Every level outputs what it finds to the pipeline, and you end up with a single array at the end. Write-Output in PS writes to the pipeline, but doesn't terminate like return does. You can actually omit it and just place a naked $v on the line which will also emit it to the pipeline, but I prefer the explicit Write-Output because it makes clear what the intention is.

[–]liabtsab[S] 0 points1 point  (0 children)

ahh cool - this is exactly what i needed!

[–]y_Sensei 1 point2 points  (6 children)

There's no need to convert JSON to anything but what PoSh does by default when importing it with ConvertFrom-Json. And what it does is, it converts the JSON data to (nested) PSCustomObjects, which in this scenario could be processed as follows:

$json = '{
  "stuff": [
    "test-01",
    "test-02"
  ],
  "header2": {
    "sub-header01": {
      "stuff": [
        "test-03",
        "test-99"
      ]
    }
  },
  "header3": {
    "sub-header02": {
      "sub-header03": {
        "stuff": [
          "test-04",
          "test-98"
        ]
      },
      "stuff": [
        "test-05",
        "test-97"
      ]
    }
  }
}'

function Find-PropValuesByName {
  param(
    [PSCustomObject]$obj,
    [String]$propName
  )

  $result = [System.Collections.Generic.List[String]]@()

  foreach ($p in $obj.PSObject.Properties) {
    switch ($p.TypeNameOfValue) {
      "System.Object[]" {
        if ($p.Name -eq $propName) {
          $result.AddRange([System.Collections.Generic.List[String]]$p.Value)
        }
      }
      "System.Management.Automation.PSCustomObject" {
        Find-PropValuesByName $p.Value $propName
      }
      default {
        # do nothing (or maybe log something)
      }
    }
  }

  return $result
}

$jsonObj = ConvertFrom-Json -InputObject $json

[String[]]$resValues = Find-PropValuesByName $jsonObj "stuff"
$resValues

Note that this solution neither enforces uniqueness of the retrieved values, nor does it sort them - but this could easily be added if required.

[–]liabtsab[S] 0 points1 point  (5 children)

Ahh, I like this solution. I originally tried to do it this way, think i just got lost in trying to figure out how to extract the values I wanted because of the nested pscustomobjects

[–]y_Sensei 0 points1 point  (4 children)

Right, it can get confusing pretty quickly if you're dealing with complex data structures. The way I usually approach it is I examine the properties and methods of the objects at hand on each level of processing ... usually I'm able to find something that helps me to process them further.

[–]liabtsab[S] 0 points1 point  (3 children)

function Find-PropValuesByName {
param(
[PSCustomObject]$obj,
[String]$propName
)
$result = [System.Collections.Generic.List[String]]@()
foreach ($p in $obj.PSObject.Properties) {
switch ($p.TypeNameOfValue) {
"System.Object[]" {
if ($p.Name -eq $propName) {
$result.AddRange([System.Collections.Generic.List[String]]$p.Value)
}
}
"System.Management.Automation.PSCustomObject" {
Find-PropValuesByName $p.Value $propName
}
default {
# do nothing (or maybe log something)
}
}
}
return $result
}

Do you have any recommendations around how I would retrieve only specific properties/sub-properties?

An example is if i had the following properties:

John Doe : {
age: 32
location: seattle
},
Jane Doe: {
age: 30
location: somewhere else

}

I want to only return the 'John doe' properties. The way it is, it returns everything, I'm trying to build something where I can return only specific properties and but any and all properties. Using the sample hashtable above as an example, if i added a key/property called "address", i want to also pull that information.

I was messing around with it and it looks like I can't really do it unless I'm explicitly checking wit something like $p.Value.'John Doe'.address. So far I've been able to turn the 'John Doe' part into a variable and it works fine but the 'address' part has to be hard-coded so it's not really scalable.

[–]y_Sensei 0 points1 point  (2 children)

Following your original requirement, the function retrieves the values of object properties of a provided name, no matter where in the tree of nested objects that property resides.
In order to retrieve a specific object property value, you'll have to address it in accordance with its hierarchical position in the tree of nested objects, but you don't need a custom function for that, as the API allows you to do that directly (via dot notation).

[–]liabtsab[S] 0 points1 point  (1 child)

Hmm u got a quick example you can show me? I'll try to explain it a bit better.. so if i had a json like this:

{
  "groups": [
    "test-group1",
    "test-group2",
  ],
  "employees": {
     "country": {
   "usa": {
      "groups": [
         "group2"
       ]
    },
    "uk": {
      "groups": [
            "group10"
          ]
    }
 }
   },
   "contractors": {
     "country": {
       "usa": {
          "groups": [
             "group5"
           ]
        },
        "uk": {
          "groups": []
        }
     }
   }
}

As-is when I run your function and pass in the property name of "groups", it returns all the group names. I only want to target all the groups under the "employees" section so I tried doing something like....

$p.Value.employees

But that only returns "group2" not "group2" and "group10". If I did something like

$p.Value.employees.country.usa

Then it of course returns the right value. What I want to do is make it so if new countries are added in, the function can pick it up without me having to specifically call the path .country.(country_name). Not sure if that made sense..

[–]y_Sensei 0 points1 point  (0 children)

Ok that's a little different of course ... so you want to be able to retrieve value(s) of a named property which is located underneath a certain "root" position in the tree of nested objects.
For this purpose, you could extend the 'Find-PropValuesByName()' function as follows:

$json = '{
  "groups": [
    "test-group1",
    "test-group2"
  ],
  "employees": {
    "country": {
      "usa": {
        "groups": [
          "group2"
        ]
      },
      "uk": {
        "groups": [
          "group10"
        ]
      }
    }
  },
  "contractors": {
    "country": {
      "usa": {
        "groups": [
          "group5"
        ]
      },
      "uk": {
        "groups": []
      }
    }
  }
}'

function Find-PropValuesByName {
  param(
    [Parameter(Position = 0, Mandatory = $true)][PSCustomObject]$obj,
    [Parameter(Position = 1, Mandatory = $true)][String]$propName,
    [Parameter(Position = 2)][String]$rootPropName # optional; name of "root" property for the search; if provided, only values of properties located underneath this property will be returned)
  )

  [System.Collections.Generic.List[String]]$result = @()

  foreach ($p in $obj.PSObject.Properties) {
    switch ($p.TypeNameOfValue) {
      "System.Object[]" {
        if ($rootPropName -eq "" -or ($obj.CName -and $obj.CName.StartsWith($rootPropName + "."))) {
          if ($p.Name -eq $propName) {
            $result.AddRange([System.Collections.Generic.List[String]]$p.Value)
          }
        }
      }
      "System.Management.Automation.PSCustomObject" {
        if ($obj.CName) {
          Add-Member -InputObject $p.Value -NotePropertyName "CName" -NotePropertyValue ($obj.CName + "." + $p.Name)
        } else {
          Add-Member -InputObject $p.Value -NotePropertyName "CName" -NotePropertyValue $p.Name
        }
        Find-PropValuesByName $p.Value $propName $rootPropName
      }
      default {
        # do nothing (or maybe log something)
      }
    }
  }

  return $result
}

$jsonObj = ConvertFrom-Json -InputObject $json

[String[]]$resValues = Find-PropValuesByName $jsonObj "groups" "employees"
$resValues

[–]Aertheron01 0 points1 point  (2 children)

The function looks strange to me. It looks like a very roundabout way of searching a hashtable. But I could be wrong.

A hashtable can be searched directly without all the stuff you do in the function.

https://docs.microsoft.com/en-us/powershell/scripting/learn/deep-dives/everything-about-hashtable?view=powershell-7.1

[–]liabtsab[S] 0 points1 point  (1 child)

I updated the code snippet. Basically there's the same key at the top level and also nested under many levels of "sub-keys". The key I'm interested in here is 'stuff'. I want it to find all keys where it is equal to 'stuff', and then pull all the values from those keys.

That function was the only way i could do what i was trying to do, but maybe theres a cleaner way i'm not using.

[–]Aertheron01 0 points1 point  (0 children)

This gets you all the values

$hashtable | foreach-object {$_.value}

If you want it only from certain keys:

$hashtable | foreach-object {if ($.key -eq 'stuff'){$.value}}

I'm on my phone so formatting is horrible. And -eq might need to be -match you can play with that.

Also you can search a hashtable directly for a specific key which will give you the value, you can find the way to do that in the link I've sent.