Powershell Where-Object doesn't seem to filter

Viewed 44

I'm trying to do some reporting on Azure Policies. I'll eventually be filtering on dates, but having trouble filtering on anything, so present the following sample.

PS C:\>$defstrings = az policy definition list --management-group "mgsandbox"   # returns an array of strings
PS C:\>$def = ConvertFrom-Json -InputObject ($defstrings -join "`n") -depth 99  # converts to an array of PSCustomObject
PS C:\>$def.count
2070
PS C:\>$sel = Where-Object -inputobject $def -FilterScript { $_.displayName -eq "Kubernetes cluster containers should not share host process ID or host IPC namespace" }
PS C:\>$sel.count
2070
PS C:\> $def[0].displayName -eq "Kubernetes cluster containers should not share host process ID or host IPC namespace"
False

While I might possibly find more than one hit on the displayName, there are clearly a non-zero set of displayNames that do not match the filter, yet the selection is getting all of them.

Any suggestions what's wrong with my syntax? It seems straightforward.

1 Answers

Do not use an -InputObject argument with Where-Object; instead, provide input via the pipeline:

# Use the pipeline to provide input, don't use -InputObject
$def | Where-Object -FilterScript { $_.displayName -eq "Kubernetes cluster containers should not share host process ID or host IPC namespace" }

In most cmdlets, the -InputObject parameter is a mere implementation detail whose purpose is to facilitate pipeline input and cannot be meaningfully used directly; see this answer for more information.


As for what you tried:

When you use -InputObject, an argument that is a collection (enumerable) is passed as a whole to the cmdlet, whereas using the same in the pipeline cause its enumeration, i.e. the collection's elements are passed, one by one.

A simplified example:

# Sample array.
$arr = 1, 2, 3

# WRONG: array is passed *as a whole*
#        and in this case outputs *all* its elements.
#        -> 1, 2, 3
Where-Object -InputObject $arr { $_ -eq 2 } 

That is, the script block passed to Where-Object is executed once, with the automatic $_ variable bound to the array as a whole, so that the above is in effect equivalent to:

if ($arr -eq 2) { $arr }

Since $arr -eq 2 evaluates to $true in a Boolean context (the if conditional), $arr as a whole is output (although on output it is enumerated), giving the impression that no filtering took place.

  • The reason that $arr -eq 2 evaluates to $true is that the -eq operator, among others, supports arrays as its LHS, in which case the behavior changes to filtering, by returning the sub-array of matching elements instead of a Boolean, so that 1, 2, 3 -eq 2 yields @(2) (an array containing the one matching element, 2), and coercing @(2) to a Boolean yields $true ([bool] @(2)).[1]

Conversely, if the implied conditional yields $false (e.g., $_ -eq 5), no output is produced at all.

By contrast, if you use the pipeline, you'll get the desired behavior:

# Sample array.
$arr = 1, 2, 3

# OK: Array elements are enumerated, i.e. 
#     sent *one by one* through the pipeline.
#     -> 2
$arr | Where-Object{ $_ -eq 2 } 

Alternatively, you can bypass the pipeline by using the intrinsic .Where() method:

Note: This requires collecting all input in memory first; however, especially with data already in memory, this approach performs better than the pipeline approach:

# OK:
# -> 2 (wrapped in a collection)
@(1, 2, 3).Where({ $_ -eq 2 })

Note: .Where() always outputs an array-like collection, even when only a single object matches the filter. In practice, however, that usually doesn't matter.


[1] For a summary of PowerShell's to-Boolean coercion rules, see the bottom section of this answer.

Related