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.| 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 } 6 7 8 9 10 PS C:\>
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. |
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 6 7 8 9 10 PS /home/joakim/Documents/wiki> 'a', 'ab', 'bb', 'ac', 'ad', 'b', 'c' -match 'a.' ab ac ad
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)} ParserError: 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 A_pri Acces Acces Activ Aksje Ascii Autom Bitco Calcu
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 How_to_check_perl_module_version.php 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) } >> 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>
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 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*' False PS E:\temp> [bool]'foo' True PS E:\temp> [bool]'' False PS E:\temp>
But I digress...
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.
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
$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.
PowershellMinimum 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.