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.