Jump to page sections
Initial date of writing: Some time around 2012-2016. Last update: 2022-01-07.

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 - but it's considered a best practice to write it out fully as "Where-Object" in "production" scripts.

This article is primarily written about PowerShell version 2.

The Where-Object Cmdlet's Syntax

The syntax is as follows:
 | Where-Object {  } [| ]

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. You can separate statements with semicolons (they work sort of like newlines in a script).

Here is a basic example:
PS C:\> 1..10 | Where-Object { $_ -gt 5 }
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 numerical 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 wildcard matching 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.

Using Operators Directly On Collections/Arrays

Be aware that you can use the operators directly on collections/arrays, but the behaviour is then different from when you use it on singular elements. Instead of returning true or false once, it acts as a filter that lets through elements that match, or rather where the condition with the current element evaluates to "true" (boolean logic).

Here are demonstrations of -match and -gt acting as filters on the collection 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10.

PS /home/joakim/Documents/wiki> 0..10 -gt 5

PS /home/joakim/Documents/wiki> 'a', 'ab', 'bb', 'ac', 'ad', 'b', 'c' -match 'a.'


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. With that said, some say "explicit is better than implicit", at least in the Python camp. Some find it easier to read the explicit versions. The choice is yours - or that of the team you're collaborating with.
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.

Here is a basic example. I accidentally left out the parentheses for Test-Path the first time. Included for educational purposes.

PS /home/joakim/Documents/wiki> Get-ChildItem -File | Where-Object -FilterScript {
    $_.Extension -eq '.md' -and Test-Path -LiteralPath "$($_.BaseName).php"} | 
    % {$_.Name.SubString(0, 5)}

Line |
   1 |  … ile | Where-Object -FilterScript {$_.Extension -eq '.md' -and Test-Pa …
     |                                                                 ~
     | You must provide a value expression following the '-and' operator.

PS /home/joakim/Documents/wiki> Get-ChildItem -File | Where-Object -FilterScript {
    $_.Extension -eq '.md' -and (Test-Path -LiteralPath "$($_.BaseName).php")} |
    % {$_.Name.SubString(0, 5)} | 
    Select-Object -First 10

A Loo

Here is an example where I filter out *.php files smaller than 4 kB. And I accidentally make apparent the somewhat atrocious approach to converting the old wiki to HTML/PHP.. *monkey emoji* 🙈

PS /home/joakim/Documents/wiki> gci | ?{$_.Extension -eq '.php' -and $_.Length/1024 -lt 4} | % Name

PS /home/joakim/Documents/wiki> 

This next 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 from the file system. 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) }
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 and logically 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

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 below, "$false" stringified into "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*'
PS E:\temp> [bool]'foo'
PS E:\temp> [bool]''
PS E:\temp>

But I digress...

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.

Filter 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

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]$' }

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]$' }

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.

As the years have gone on and it is now 2022 when I'm adding this comment to the article, in a world where people are using PowerShell 7 / PowerShell Core, and PowerShell 2 is rather old, there are optimizations in place for scenarios like this built into the language components themselves, but I would have to benchmark to see if it's in play here, and perhaps in different ways/scenarios.

Powershell      Windows          All Categories

Google custom search of this website only

Minimum cookies is the standard setting. This website uses Google Analytics and Google Ads, and these products may set cookies. By continuing to use this website, you accept this.

If you want to reward my efforts