PowerShell Where-Object gotcha

When using Where-Object in script block form, e.g Where-Object {$_.LastWriteTime -gt (Get-Date).AddHours(-1)}, it is inefficient because the Get-Date command will be executed for every single pipeline element. In general it’s recommended to use precomputed values for filtering in script block form: $OneHourAgo = (Get-Date).AddHours(-1)
Get-ChildItem -File | Where-Object { $_.LastWriteTime -gt$OneHourAgo }


However, in the newer form that doesn’t require the use of $psitem / $_, the command will only run once:

# test function which logs when it gets called
PS C:\> function test { write-verbose -Verbose -Message $(get-date) ; get-date } # parameter form, it logs being called once PS C:\> gci | where LastWriteTime -gt (test).addhours(-1) VERBOSE: 14/01/2020 00:08:52 # scriptblock form, it logs being called many times PS C:\> gci | where {$_.LastWriteTime -gt (test).addhours(-1)}
VERBOSE: 14/01/2020 00:09:15
VERBOSE: 14/01/2020 00:09:15
VERBOSE: 14/01/2020 00:09:15
[..]


Explanation

A script block is a bit like a function which doesn’t have a name. When passed to a Where-Object cmdlet it is called in its entirety once for each input object, and so everything inside it is evaluated for each input.

The other form looks like a script block:

Where-Object LastWriteTime -gt (Get-Date).AddHours(-1)


…but it’s not. This is just a cmdlet being called with two positional arguments and one switch. The first argument is the string 'LastWriteTime', it binds to the first positional parameter (which is named -Property). This is no different than running Get-ChildItem name*, where the string 'name*' is bound to its first positional parameter (named -Path).

The second portion looks like the -gt operator, but it’s actually a cmdlet switch named -gt.

The last argument is evaluated once, the same as any cmdlet argument (and in this case into a [DateTime] object), and then bound to the second positional parameter (which is named -Value).

The Where-Object call above is no different from:

Where-Object -Property LastWriteTime -gt -Value (Get-Date).AddHours(-1)


Since PowerShell doesn’t care about the order of named arguments, a sadist could also write:

Where-Object -Value (Get-Date).AddHours(-1) -gt LastWriteTime


to unexpectedly produce the same results as the first two.

Last updated