2

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 Answers1

1

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 well as GitHub issue #4242 for a discussion.


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.

mklement0
  • 382,024
  • 64
  • 607
  • 775