如何在对象中搜索值?

How to search an object for a value?

假设您有一个巨大的对象 - 可能有也可能没有嵌套数组/对象,

# Assuming 'user1' exists in the current domain    
$obj = Get-ADUser 'user1' -Properties *

我想在该对象中搜索不区分大小写的字符串 SMTP...

我试过的

$obj | Select-String "SMTP"

但它不起作用,因为匹配项位于嵌套的 Collection 中...简而言之,它位于 属性 $obj.proxyAddresses 中。

如果我运行$obj.proxyAddress.GetType()它returns:

IsPublic IsSerial Name                      BaseType
-------- -------- ----                      --------
True     False    ADPropertyValueCollection System.Collections.CollectionBase

解决此问题的最佳方法是什么?我知道您可以遍历属性并使用通配符匹配或 .Contains() 手动查找它,但我更喜欢内置解决方案。

因此,它将是对象的 grep 而不仅仅是字符串。

注意:此答案包含背景信息并提供快速入门不需要自定义功能的方法.
有关基于 自定义函数 反射的更 更彻底、更系统的方法 ,请参阅

Select-Stringstrings 进行操作,当它将不同类型的输入对象强制转换为字符串时,它实际上调用了 .ToString(),这通常会产生通用表示,例如纯粹的类型名称,通常 而不是 属性的枚举。
请注意,对象的 .ToString() 表示 而不是 与 PowerShell 到控制台的默认输出相同,后者更加丰富。

如果您要查找的只是在对象的 for-display 字符串表示中找到一个子字符串,您可以在管道传输到 Select-String 之前管道传输到 Out-String -Stream:

$obj | Out-String -Stream | Select-String "SMTP"

Out-String 创建一个 string 表示,它与默认情况下呈现给 console 的表示相同(它使用 PowerShell 的输出格式化系统);添加 -Stream 逐行发出表示 ,而默认情况下发出单个多行字符串。

注意:最新版本的 PowerShell 带有 便捷函数 osswraps Out-String -Stream:

$obj | oss | Select-String "SMTP"

当然,此方法仅在用于显示的表示实际显示感兴趣的数据时才有效 - 请参阅下面的注意事项。

也就是说,在显示表示中搜索可以说是Select-String应该做的默认 - 参见GitHub issue #10726

注意事项:

  • 如果格式化表示恰好是 tabular 并且您的搜索字符串是 属性 name , 感兴趣的值可能在 下一个 行。

    • 您可以通过强制使用 list 样式的显示来解决这个问题 - 其中每个 属性 占据自己的一行(名称和值)- 如下:

       $obj | Format-List | Out-String -Stream | Select-String "SMTP"
      
    • 如果您预期多行 属性 值,您可以 使用 Select-String-Context 参数来包含行 周围匹配,例如-Context 0,1在匹配

      之后也输出行
    • 如果您知道感兴趣的值在 collection-valued 属性 中,您可以使用$FormatEnumerationLimit = -1强制列出所有个元素(默认情况下,只显示前4个元素)。

      • 警告:从 PowerShell Core 6.1.0 开始,$FormatEnumerationLimit 只有在 global 范围内设置才有效 - 请参阅 this GitHub issue.
      • 但是,一旦您需要设置首选项变量 $FormatEnumerationLimit,就该考虑基于 中的自定义函数的更彻底的解决方案了。
  • 值在表示中可能会被截断,因为Out-String假设线宽固定;你可以使用 -Width 来改变它,但要小心大数字,因为表格表示然后使用每个输出行的完整宽度。

这是一种解决方案。根据您搜索的深度,它可能会非常慢;但是 1 或 2 的深度很适合您的场景:

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]::Property)

    )
    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

示例结果:

Value                                           Name                                  
-----                                           ----                                  
smtp:SomeOne@myCompany.com                      InputObject.msExchShadowProxyAddresses
SMTP:some.one@myCompany.co.uk                   InputObject.msExchShadowProxyAddresses
smtp:username@myCompany.com                     InputObject.msExchShadowProxyAddresses
smtp:some.one@myCompany.mail.onmicrosoft.com    InputObject.msExchShadowProxyAddresses    
smtp:SomeOne@myCompany.com                      InputObject.proxyAddresses  
SMTP:some.one@myCompany.co.uk                   InputObject.proxyAddresses  
smtp:username@myCompany.com                     InputObject.proxyAddresses  
smtp:some.one@myCompany.mail.onmicrosoft.com    InputObject.proxyAddresses     
SMTP:some.one@myCompany.mail.onmicrosoft.com    InputObject.targetAddress  

说明

Find-ValueMatchingCondition 是一个函数,它接受给定的 object (InputObject) 并根据给定条件递归地测试其每个属性。

功能分为两部分。第一部分是根据条件测试输入 object 本身:

if ($InputObject | Where-Object -FilterScript $Condition) {
    New-Object -TypeName 'PSObject' -Property @{Name=$Name;Value=$InputObject}
}

这表示,$InputObject 的值与给定的 $Condition 相匹配,然后 return 具有两个属性的新自定义 object; NameValueName 是输入 object 的名称(通过函数的 Name 参数传递),而 Value 如您所料,是 object'值。如果 $InputObject 是一个数组,则数组中的每个值都会被单独评估。传入的根名称object默认为"InputObject";但是您可以在调用函数时将此值覆盖为任何您喜欢的值。

函数的第二部分是我们处理递归的地方:

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);$_}
    }
}

If 语句检查我们对原始 object 的了解有多深(即,由于 object 的每个属性都可能有自己的属性,因此可能无限级别(因为属性可能会指向 parent),最好限制我们可以到达的深度。这与 ConvertTo-JsonDepth 参数的目的基本相同。

If 语句还检查 object 的类型。即对于大多数原始类型,该类型保存值,我们对它们 properties/methods 不感兴趣(原始类型没有任何属性,但有各种方法,可以根据 $PropertyTypeToSearch).同样,如果我们正在寻找 -Condition {$_ -eq 6},我们不会想要所有长度为 6 的字符串;所以我们不想深入了解字符串的属性。此过滤器可能会进一步改进以帮助忽略其他类型/我们可以更改函数以提供另一个可选的脚本块参数(例如 $TypeCondition)以允许调用者在运行时根据他们的需要对其进行优化。

在测试是否要深入了解该类型的成员之后,我们将获取成员列表。这里我们可以使用 $PropertyTypesToSearch 参数来改变我们搜索的内容。默认情况下,我们对 Property 类型的成员感兴趣;但我们可能只想扫描 NoteProperty 类型的那些;特别是在处理自定义 object 时。有关此提供的各种选项的更多信息,请参阅 https://docs.microsoft.com/en-us/dotnet/api/system.management.automation.psmembertypes?view=powershellsdk-1.1.0

一旦我们选择了我们希望检查的输入object中的members/properties,我们依次获取每个,确保它们不为空,然后递归(即调用Find-ValueMatchingCondition).在此递归中,我们将 $Depth 减一(即,因为我们已经下降了 1 级并且我们在 0 级停止),并将此成员的名称传递给函数的 Name 参数。

最后,对于任何 returned 值(即由函数的第 1 部分创建的自定义 object,如上所述),我们在当前的 $Name 前面添加InputObject 为 returned 值的名称,然后 return 此修改为 object。这确保每个 object returned 都有一个 Name 表示从根 InputObject 到符合条件的成员的完整路径,并给出匹配的值。