Comparison Operators, Collections, and Conditionals (Oh, My!)

Earlier today, I came across this bit of code in a forum post (modified slightly for clarity):

Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled = True" |
Where-Object { $_.IPAddress | ForEach-Object { $_ -match '^192\.168\.1\.' } }

At first glance, it seems reasonable. Win32_NetworkAdapterConfiguration.IPAddress is a multi-valued property, so the author included a foreach loop in the Where-Object script block. If the NIC has an IP address matching the regular expression, the Where-Object block should evaluate to True, giving the desired result, right?

Not quite. This code works fine if all of your network adapters have only a single IP address, but this Where-Object clause actually evaluates to True for any adapter that has more than one IP, regardless of whether any of them match the pattern. For instance, when I change the pattern to something nonsensical and run it on my PC, here’s what I get:

Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled = True" |
Where-Object { $_.IPAddress | ForEach-Object { $_ -match 'Jabberwocky' } }

DHCPEnabled      : True
IPAddress        : {, fe80::8dfb:e1df:bea4:3e78}
DefaultIPGateway : {}
DNSDomain        : 
ServiceName      : e1cexpress
Description      : Intel(R) 82579V Gigabit Network Connection
Index            : 7

DHCPEnabled      : False
IPAddress        : {, fe80::ad35:fe41:f21b:ee0b}
DefaultIPGateway : 
DNSDomain        : 
ServiceName      : VBoxNetAdp
Description      : VirtualBox Host-Only Ethernet Adapter
Index            : 13

Here’s why:

When you use the Where-Object cmdlet, PowerShell executes the script block, and coerces the results to type [bool]. When a network adapter contains more than one IP address, the block { $_.IPAddress | ForEach-Object { $_ -match ‘Jabberwocky’ } } produces a multi-element array of Boolean values (which, in this case, are all False). When PowerShell casts an array to a Boolean, there are three possible results:

  • If the array is empty, it evaluates to False.
  • If the array contains one element, that element is casted to Boolean.
  • If the array contains two or more elements, it evalutes to True

You can see this for yourself by entering these tests commands in a PowerShell console:

[bool] @()  # False
[bool] @($false)   # False
[bool] @($true)   # True
[bool] @($false, false)   # True

This doesn’t just affect the Where-Object cmdlet; the same rules apply in an “if” statement, or the condition of a while / do loop. So how do we fix it?

As always, there are multiple ways to fix the problem. In this post, I’m going to focus on how the comparison operators behave when the left operand is a collection. Instead of returning a Boolean value, they act as a sort of filter themselves, returning only the elements from the collection that meet the criteria of the operator. For example:

$array = @(0,1,2,3,4,5,6,7,"dog","cat","doghouse")

$array -eq 5   # This returns a single-element array containing 5
$array -lt 5   # This returns an array containing 0,1,2,3,4
$array -like 'do*'  # This returns an array containing "dog","doghouse"
$array -eq 47   # This returns an empty array.  (note: NOT $null)

When used in a conditional, the previous rules apply. 0 matches (an empty array) means False, 2 or more matches means True, and a single match depends on what you were searching for. For example, ($array -eq 0) evaluates to False in the above array, not because “0” wasn’t found, but because 0 happens to convert to False. (On a side note, it would be better to use the -contains operator in this situation rather than -eq, for that reason.)

Going back to the initial example with network adapters, an easy solution is simply to get rid of the ForEach loop:

Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled = True" |
Where-Object { $_.IPAddress -match '^192\.168\.1\.' }

Now, what happens?

  • If zero IPs in the list match the pattern, you get an empty array (which evaluates to False).
  • If one IP matches, you get back that IP address as a string, and this non-empty string will always evaluate to True when cast to a Boolean.
  • If more than one IP matches, you get a multi-element array, which also evaluates to True.

Exactly the results you wanted for that Where-Object filter.

The “about_Comparison_Operators” help file contains the details on how these operators behave for both scalar (single) values and collections. When you’re writing your script’s logic, be sure to ask yourself: “Can this value I’m evaluating sometimes be a collection?”, and hopefully, this type of bug will not bite you.

3 thoughts on “Comparison Operators, Collections, and Conditionals (Oh, My!)

  1. PM

    This functionality has now changed in PowerShell 4.0, [bool] @($false, $false) will return a False result.

    1. Dave Wyatt Post author

      I’ve only just installed WMF 4.0, but it doesn’t seem to have changed:

      PS C:\Users\Dave> $PSVersionTable

      Name Value
      —- —–
      PSVersion 4.0
      WSManStackVersion 3.0
      CLRVersion 4.0.30319.18052
      BuildVersion 6.3.9600.16406
      PSCompatibleVersions {1.0, 2.0, 3.0, 4.0}
      PSRemotingProtocolVersion 2.2

      PS C:\Users\Dave> [bool]@($false, $false)

  2. Pingback: Zajímavé novinky ze svÄ›ta IT z 44. týdne 2013 | Igorovo

Comments are closed.