The PowerShell Where-Object cmdlet

From Svendsen Tech Powershell Wiki

Jump to: navigation, search

In PowerShell there's a pipeline flow control cmdlet called Where-Object, which is used to filter elements based on some condition and whether it returns true or false. The Where-Object cmdlet accepts a script block as its argument, which means you can perform any operation you want that's possible using the PowerShell language.

The current object from the pipeline is in the automatic variable $_, just like with the ForEach-Object cmdlet. If the last expression in the script block evaluates to a true value, the object is written to the pipeline ("passed on"), else it is discarded. It's much like the grep keyword in Perl.

The Where-Object cmdlet has two aliases, which are "Where" and simply a question mark: "?". They will work equivalently to writing out the full name of the cmdlet.

This article is primarily written about PowerShell version 2.

Contents






The Where-Object Cmdlet's Syntax

The syntax is as follows:

<pipeline> | Where-Object { <ListOfStatements> } [| <optional pipeline>]

The list of statements is in the script block you pass in to Where-Object, denoted by the curly braces. If the last expression evaluates to a true value, the object in the automatic variable $_ is written to the pipeline. If the last expression turns out to be false, the object is discarded.

Here is a basic example:

PS C:\> 1..10 | Where { $_ -gt 5 }
6
7
8
9
10
PS C:\>


PowerShell Comparison Operators You'll Typically Use With Where-Object

Each of the operators in the table below can be used with either the letter "i" or "c" prepended, where "i" means case-insensitive matching (which is technically redundant since this is the default behaviour), and the "c" which indicates case-sensitive matching. Of course, case sensitivity on numeric comparisons doesn't make much sense. The PowerShell operators are overloaded and will work on strings and integers (and try to work on other types - and convert if a loss-less conversion can be performed when necessary).

So in the case of "-eq", it would be "-ieq" for explicitly case-insensitive matching, and "-ceq" for case-sensitive matching.

-eq Equal to.
-ne Not equal to.
-lt Less than.
-le Less than or equal to.
-gt Greater than.
-ge Greater than or equal to.
-like Uses wildcard matching. Read more about it here.
-notlike Returns true when wildcard pattern does not match. Read more about the patterns here.
-match Uses regular expressions. Read more about PowerShell regular expressions here.
-notmatch Returns true when the regular expression does not match.

A Where-Object Cmdlet Example With Multiple Conditions

To list all directories that start with the letter "t" in a directory, you can use Where-Object and filter on the "PSIsContainer" property as well as checking the "Name" property, and making sure it begins with a t. You will often see people using redundant "-eq $true" or "-eq $false" checks, such as in this example where you might see "$_.PSIsContainer -eq $true", but it's already a value that will either be true or false, so that's not necessary.

PS C:\> dir E:\temp | Where-Object { $_.PSIsContainer -and $_.Name -like 't*' }


    Directory: E:\temp


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d----        20.07.2011     17:12            target
d----        20.11.2010     11:05            test
d----        30.01.2010     12:29            Thomson Speedtouch
d----        20.07.2010     03:36            thunderbird test
d----        17.07.2012     22:07            tucan


I should also mention that you will often need to add parentheses to group expressions on either side of the -and and -or operators. This is necessary for instance if you use a cmdlet like Test-Path. This example seems a bit useless and unlikely to be needed in the real world, but it does demonstrate a few things I'd like to mention.

Here I list everything that starts with "test" in the E:\temp directory, save it in testfiles.txt, and then I delete testlogo.png. Finally, I run a one-liner that gets a directory element ($de) for each string in the file, using Get-Item ("dir" is an alias for Get-ChildItem). I check if this is not null by simply using "$de", because null/$null is considered false - and the last condition that has to be true, is that the file exists and is not a directory. This excludes "test" because it's a directory, and testlogo.png because it no longer exists.

PS E:\temp> dir test* | select -expand Name > testfiles.txt
PS E:\temp> (gc .\testfiles.txt) -join ', '
test, test.cmd, test.pl, test.ps1, test.txt, test.zip, testfiles.txt, testlogo.png
PS E:\temp> del testlogo.png
PS E:\temp> gc .\testfiles.txt | ?{ $de = Get-Item $_ -ErrorAction SilentlyContinue;
>> $de -and (Test-Path -PathType Leaf $de.FullName) }
>>
test.cmd
test.pl
test.ps1
test.txt
test.zip
testfiles.txt
PS E:\temp>

If you were to try without the parentheses around Test-Path, you'd get this error:

PS E:\temp> gc .\testfiles.txt | ?{ $de = Get-Item $_ -ErrorAction SilentlyContinue;
>> $de -and Test-Path -PathType Leaf $de.FullName }
>>
You must provide a value expression on the right-hand side of the '-and' operator.
At line:1 char:82
+ gc .\testfiles.txt | ?{ $de = Get-Item $_ -ErrorAction SilentlyContinue; $de -and <<<<  Test-Path -PathType Leaf $de.FullName }
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ExpectedValueExpression

PS E:\temp>

Where-Object Examples That Exclude Using The -not Operator

To negate the logic for directories in the above example and have it match files instead (that start with "te"), you can add the -not operator (you could also use an exclamation point: "!" - as it's functionally equivalent).

PS C:\> dir E:\temp | Where { -not $_.PSIsContainer -and $_.Name -like 'te*' }


    Directory: E:\temp


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        07.09.2011     05:49         37 tekst.txt
-a---        03.08.2012     03:08       1803 temp.csv
-a---        29.07.2011     03:42        257 test.cmd
-a---        23.11.2009     19:31         22 test.pl
-a---        09.07.2012     03:51        206 test.ps1
-a---        08.03.2012     21:45          5 test.txt
-a---        18.05.2011     00:11        156 test.zip
-a---        08.07.2012     15:06       5393 testlogo.png

Something we all learn at some point with PowerShell, is that parentheses are sometimes necessary for grouping (and sometimes for introducing "code context"), and with a -not negation in front of $foo -like 'bar' - or something similar, you will need to add parentheses. This is due to the precedence of -not being very high, so it sort of "binds tightly" to its right operand.

Here's a demonstration that works, with parentheses added. Not the outside parentheses, those are to retrieve the count, so I get the count of files not starting with t, rather than having to paste 157 lines here... :-)

PS E:\temp> ( dir . | ? { -not ($_.Name -like 't*') } ).Count
157

If you try without the parentheses, it will basically check if the result of "-not $_.Name", a boolean, matches the pattern specified by -like ('t*'). This doesn't make sense, and as you can see, $false doesn't match 't*'. So the expression never becomes true, because strings are always true, except for empty ones.

PS E:\temp> (dir . | ? { -not $_.Name -like 't*' }).Count
PS E:\temp> dir . | ? { -not $_.Name -like 't*' }
PS E:\temp> dir . | ? { (-not $_.Name) -like 't*' }
PS E:\temp> $false -like 't*'
False
PS E:\temp> [bool]'foo'
True
PS E:\temp> [bool]''
False
PS E:\temp>

Optimized Single-parameter Check in PowerShell v3

In PowerShell version 3 (introduced with Windows Management Framework v3 in 2012), they added a shorter way of writing single-parameter checks, where you omit the script block, like this:

PS C:\> Get-CimClass Win32_ComputerSystem | Select -Expand CimClassMethods |
>> Where Qualifiers -match 'Implemented' | Format-Table Name, Qualifiers -a
>>

Name                    Qualifiers
----                    ----------
Rename                  {Implemented, ValueMap}
JoinDomainOrWorkgroup   {Implemented, ValueMap}
UnjoinDomainOrWorkgroup {Implemented, ValueMap}

In PowerShell version 2 and earlier, you have to write the Where-Object part like this, using a script block:

[...] | Where { $_.Qualifiers -match 'Implemented' } | [...]

If you need multiple conditions or more complex code, you will have to use the "old" script block style.


A Where-Object Example That Filters Based On A Regexp Match

To filter using a regexp, you just use the -match, -imatch or -cmatch operator as you normally would (with PowerShell v2 and up). Here I target all files and directories that start with the letter t, and retrieve the count property on the resulting collection (System.Object[] array). Read more about PowerShell regular expressions here.

PS C:\> (dir E:\temp | Where { $_.Name -imatch '^t' }).Count
22

Here I get only the strings that are composed of one single hex digit:

PS C:\> 'a', 'aa', '1', 'x', 'y', 'b', '2' | ? { $_ -imatch '^[a-f0-9]$' }
a
1
b
2

To invert/reverse/negate this and get the ones that do not match, you can replace the -imatch operator with the -iNotMatch operator (or -NotMatch since it's case-insensitive by default).

PS C:\> 'a', 'aa', '1', 'x', 'y', 'b', '2' | ? { $_ -NotMatch '^[a-f0-9]$' }
aa
x
y

Where-Object Used With A Script/Function Switch

Something I've found myself doing quite a bit, is having Where-Object filters that should only be in effect if a switch parameter to the script or function has been specified. Let's say you have the switch -DoNotFilterBuiltin and you want to filter unless this is specified, but not when it is specified. To do this, I've been using something like this:

$Collection | Where { if ($DoNotFilterBuiltin) { $true } else { $Exceptions -Contains $_.Property } }
 | ForEach-Object { ...

There it would always return true if the switch parameter was specified, and if not, it would return either true or false depending on whether the $Exceptions array contained the value in $_.Property.

However, using this method will be slower than writing one if statement and two separate, different pipelines, so you only check the variable once. This type of repeated check of a variable tends to slow things down in interpreted languages.

Personal tools