Why doesn't $PSItem behave as expected when using a bracket-based -Filter argument?
Asked Answered
W

2

5

I was assisting a user with this question, linked to my answer here: Powershell script to add users to A/D group from .csv using email address only?

Initially I wrote the script as follows, using a bracket-based filter for Get-AdUser like follows:

Import-CSV "C:\users\Balbahagw\desktop\test1.csv" | 
  Foreach-Object {

    # Here, $_.EmailAddress refused to resolve
    $aduser = Get-ADUser -Filter { EmailAddress -eq $_.EmailAddress }

    if( $aduser ) {
      Write-Output "Adding user $($aduser.SamAccountName) to groupname"
      Add-ADGroupMember -Identity groupname -Members $aduser
    } else {
      Write-Warning "Could not find user in AD with email address $($_.EmailAddress)"
    }
  }

However, $_.EmailAddress failed to populate a value. However, changing the Get-ADUser filter to a string-based filter worked as intended:

$aduser = Get-ADUser -Filter "EmailAddress -eq '$($_.EmailAddress)'"

What is the strangeness I'm experiencing, and why? Is it because when I'm using brackets, it's treated as a new scope and the $PSItem won't follow?

Wallachia answered 2/7, 2018 at 13:34 Comment(3)
The short of it: never use script blocks as -Filter arguments: https://mcmap.net/q/467365/-get-aduser-filter-will-not-accept-a-variableSpathe
Added better example of what to specifically change in your script, it should 'Just work' now :)Antinomy
Oh, I already had it working with the change you suggested, but thanks! I was looking for why using the bracket filter wasn't working.Wallachia
S
7
  • -Filter parameters are generally string parameters (verify with
    Get-Help Get-AdUser -Parameter Filter)

    • They generally do not accept PowerShell code - filters are provider-specific and often have their own syntax, although it happens to be PowerShell-like in the case of the AD cmdlets.
      Also, they generally have no knowledge of PowerShell variables (see below).
  • Thus, when a script block ({ ... }) is passed, it is converted to a string, which evaluates to its literal contents (everything between the opening { and the closing }):

    • { EmailAddress -eq $_.EmailAddress }.ToString() yields the literal string EmailAddress -eq $_.EmailAddress - without any evaluation - and that's what Get-AdUser sees - no evaluation takes place.

    • In a presumably well-meaning but misguided effort to support the widespread, but ill-advised practice of passing script blocks to the -Filter parameter of AD cmdlets, it seems that these cmdlets actually explicitly expand simple variable references such as $_ in the string literal they receive, but that doesn't work with expressions, such as accessing a property of a variable ($_.EmailAddress)

Therefore, -Filter arguments should generally be passed as expandable strings ("..."); in the case at hand:

 -Filter  "EmailAddress -eq '$($_.EmailAddress)'"

That is, the only robust solution is to use strings with the variable parts baked in, up front, via string expansion, as shown above.

For values that are neither numbers nor strings, such as dates, you may have to use a literal string ('...') and rely on the AD provider's ability to evaluate simple references to PowerShell variables (e.g., $date) - see this answer of mine for details.

As stated, the syntax of AD filters is only PowerShell-like: it supports only a subset of the operators that PowerShell supports and those that are supported differ subtly in behavior - see Get-Help about_ActiveDirectory_Filter.

  • It is tempting to use script blocks, because the code inside requires no escaping of embedded quotes / no alternating of quote chars and no use of subexpression operator $(...). However, aside from using script blocks as strings being inefficient in general, the problem here is that the script block is making a promise that it cannot keep: it looks like you're passing a piece of PowerShell code, but you're not - and it works only in simple cases (and then only due to the misguided accommodation mentioned above); generally, it's hard to remember under what circumstances it doesn't work and how to make it work if it fails.

  • It is therefore really unfortunate that the official documentation uses script blocks in its examples.

For a more comprehensive discussion, see this answer of mine.

Spathe answered 2/7, 2018 at 14:41 Comment(2)
I also did some testing on my own and discovered that ScriptBlocks get ToString'd when passed as an argument that expects a String type (which is how .NET works). I was (incorrectly) assuming before that Get-ADUser also accepted a ScriptBlock as a -Filter argument when it in fact only accepts a String, and then transparently works with simple replacement because literally every object in .NET contains a ToString method.Wallachia
@BendertheGreatest: Indeed, but that's what I was trying to convey with the respective first sentences in the first two bullet points (just amended a little). Again I think it comes down to misguided documentation examples: seeing those naturally (but incorrectly) makes one assume that the parameter is script-block-typed.Spathe
A
2

You're not wrong, it's the module's fault

The type of payload you have to use with the -Filter parameter differs depending on which provider you're working with, a design decision which can be pretty confusing!

The output of Get-Help Get-ADUser -Parameter Filter gives you some pretty detailed examples of the different syntax options you can use with the Active Directory Provider's implementation of Filter syntax.

Here's an example:

#To get all user objects that have an e-mail message attribute, use one of the following commands:

Get-ADUser -Filter {EmailAddress -like "*"}

It looks like the ActiveDirectory provider places the specific restriction that you must wrap the input in quotes. Here's what happens when I look for my account without putting quotes around my e-mail.

Get-ADUser -Filter {EmailAddress -eq [email protected]}
Get-ADUser : Error parsing query: 'EmailAddress -eq [email protected]' 
Error Message: 'syntax error' at position: '18'.

But adding quotes? It works!

Get-ADUser -Filter {EmailAddress -eq "[email protected]"}


DistinguishedName : CN=Stephen,CN=Users,DC=FoxDeploy,DC=local
Enabled           : True
GivenName         : Stephen
Name              : Stephen
ObjectClass       : user
ObjectGUID        : 6428ac3f-8d17-45d6-b615-9965acd9675b
SamAccountName    : Stephen
SID               : S-1-5-21-3818945699-900446794-3716848007-1103
Surname           : 
UserPrincipalName : [email protected]

How to make yours work

Now, because of this confusing filter implementation, you will need to change your user lookup on line 5 to the following:

 $aduser = Get-ADUser -Filter "EmailAddress -eq `"$($_.EmailAddress)`""

We are providing the -Filter payload as a String. Next we want to use String Expansion to pull out the .EmailAddress property, so we wrap the string in $( ) to signal string expansion. Finally, the provider wants our filter comparison wrapped in quotes, so we put double quotes around it, and then escape the quotes using the backtick character.

And now it should work.

TLDR - blame the provider and blame the module, there are so many inconsistencies with the Active Directory module.

Antinomy answered 2/7, 2018 at 14:24 Comment(1)
Thanks for the detailed response. There is a further breakdown of this parameter as it related to the AD cmdlets in @mklement0's comment on my question: https://mcmap.net/q/467365/-get-aduser-filter-will-not-accept-a-variableWallachia

© 2022 - 2024 — McMap. All rights reserved.