Here's one solution. It can be very slow depending on what depth you search to; but a depth of 1 or 2 works well for your scenario:
function Find-ValueMatchingCondition {
Param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[PSObject]$InputObject
,
[Parameter(Mandatory = $true)]
[ScriptBlock]$Condition
,
[Parameter()]
[Int]$Depth = 10
,
[Parameter()]
[string]$Name = 'InputObject'
,
[Parameter()]
[System.Management.Automation.PSMemberTypes]$PropertyTypesToSearch = ([System.Management.Automation.PSMemberTypes]::Properties)
)
Process {
if ($InputObject -ne $null) {
if ($InputObject | Where-Object -FilterScript $Condition) {
New-Object -TypeName 'PSObject' -Property @{Name=$Name;Value=$InputObject}
}
#also test children (regardless of whether we've found a match
if (($Depth -gt 0) -and -not ($InputObject.GetType().IsPrimitive -or ($InputObject -is 'System.String'))) {
[string[]]$members = Get-Member -InputObject $InputObject -MemberType $PropertyTypesToSearch | Select-Object -ExpandProperty Name
ForEach ($member in $members) {
$InputObject."$member" | Where-Object {$_ -ne $null} | Find-ValueMatchingCondition -Condition $Condition -Depth ($Depth - 1) -Name $member | ForEach-Object {$_.Name = ('{0}.{1}' -f $Name, $_.Name);$_}
}
}
}
}
}
Get-AdUser $env:username -Properties * `
| Find-ValueMatchingCondition -Condition {$_ -like '*SMTP*'} -Depth 2
Example Results:
Value Name
----- ----
smtp:[email protected] InputObject.msExchShadowProxyAddresses
SMTP:[email protected] InputObject.msExchShadowProxyAddresses
smtp:[email protected] InputObject.msExchShadowProxyAddresses
smtp:[email protected] InputObject.msExchShadowProxyAddresses
smtp:[email protected] InputObject.proxyAddresses
SMTP:[email protected] InputObject.proxyAddresses
smtp:[email protected] InputObject.proxyAddresses
smtp:[email protected] InputObject.proxyAddresses
SMTP:[email protected] InputObject.targetAddress
Explanation
Find-ValueMatchingCondition
is a function which takes a given object (InputObject
) and tests each of its properties against a given condition, recursively.
The function is divided into two parts. The first part is the testing of the input object itself against the condition:
if ($InputObject | Where-Object -FilterScript $Condition) {
New-Object -TypeName 'PSObject' -Property @{Name=$Name;Value=$InputObject}
}
This says, where the value of $InputObject
matches the given $Condition
then return a new custom object with two properties; Name
and Value
. Name
is the name of the input object (passed via the function's Name
parameter), and Value
is, as you'd expect, the object's value. If $InputObject
is an array, each of the values in the array is assessed individually. The name of the root object passed in is defaulted as "InputObject"
; but you can override this value to whatever you like when calling the function.
The second part of the function is where we handle recursion:
if (($Depth -gt 0) -and -not ($InputObject.GetType().IsPrimitive -or ($InputObject -is 'System.String'))) {
[string[]]$members = Get-Member -InputObject $InputObject -MemberType $PropertyTypesToSearch | Select-Object -ExpandProperty Name
ForEach ($member in $members) {
$InputObject."$member" | Where-Object {$_ -ne $null} | Find-ValueMatchingCondition -Condition $Condition -Depth ($Depth - 1) -Name $member | ForEach-Object {$_.Name = ('{0}.{1}' -f $Name, $_.Name);$_}
}
}
The If
statement checks how deep we've gone into the original object (i.e. since each of an objects properties may have properties of their own, to a potentially infinite level (since properties may point back to the parent), it's best to limit how deep we can go. This is essentially the same purpose as the ConvertTo-Json
's Depth
parameter.
The If
statement also checks the object's type. i.e. for most primitive types, that type holds the value, and we're not interested in their properties/methods (primitive types don't have any properties, but do have various methods, which may be scanned depending on $PropertyTypeToSearch
). Likewise if we're looking for -Condition {$_ -eq 6}
we wouldn't want all strings of length 6; so we don't want to drill down into the string's properties. This filter could likely be improved further to help ignore other types / we could alter the function to provide another optional script block parameter (e.g. $TypeCondition
) to allow the caller to refine this to their needs at runtime.
After we've tested whether we want to drill down into this type's members, we then fetch a list of members. Here we can use the $PropertyTypesToSearch
parameter to change what we search on. By default we're interested in members of type Property
; but we may want to only scan those of type NoteProperty
; especially if dealing with custom objects. See https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.psmembertypes?view=powershellsdk-1.1.0 for more info on the various options this provides.
Once we've selected what members/properties of the input object we wish to inspect, we fetch each in turn, ensure they're not null, then recurse (i.e. call Find-ValueMatchingCondition
). In this recursion, we decrement $Depth
by one (i.e. since we've already gone down 1 level & we stop at level 0), and pass the name of this member to the function's Name
parameter.
Finally, for any returned values (i.e. the custom objects created by part 1 of the function, as outlined above), we prepend the $Name
of our current InputObject to the name of the returned value, then return this amended object. This ensures that each object returned has a Name representing the full path from the root InputObject down to the member matching the condition, and gives the value which matched.
grep
finds one line and shows only that line – Sonora