In "Introducing the Pipeline and ForEach" and "From One-Liner to ForEach One-Liner," I've shown you how the ForEach cmdlet and the pipeline variable $_ together let you write simple one-liners in a different way. For example, suppose bigfirm.com has an organizational unit (OU) called Machinists, and you want to change the description attribute of every user account to the text Machinist. (Yes, it’s a trivial example, but it's also easy to understand.) You could do that with this simple one-liner, of the kind that I discussed a dozen or more columns ago in "Find Users with Get-ADUser":

get-aduser -filter * -searchbase "ou=machinists,dc=bigfirm,dc=com" | set-aduser -description "Machinist"

And as I demonstrated in last month's column, you can recast that one-liner into one using ForEach and $_, like so:

get-aduser -filter * -searchbase "ou=machinists,dc=bigfirm,dc=com" | foreach {set-aduser $_ -description "Machinist"}

That second approach accomplishes exactly what the first one does, but it’s more complicated. So why do it? Simple! It’s more flexible. Let's say you want to set every user’s description not to Machinist but to something more whimsical. You want to extract each user’s first name and set his or her description to [First Name] the Machinist (e.g., Dan the Machinist). In that case, the simpler, non-ForEach one-liners I've shown you so far would fail completely—there’s just no way to do it.

The first time I tried to do something like that, my reasoning was as follows:

  1. The Active Directory (AD) attribute containing a user’s first name is called givenname.
  2. It’s an attribute, so I can retrieve it by doing some kind of Get-ADUser command and adding .givenname, as in this example, which retrieves the first name of a user named jdorn:

    $someuser = (get-aduser jdorn)
    $someuser.givenname
  3. Things in the pipeline already live in a variable (the built-in variable $_), and after get-aduser there’s a user in the pipeline, so $_.givenname should contain that user’s first name.
  4. As I've demonstrated in past columns, I can use the plus sign (+) to glue two text strings together (the programmer term is concatenate), so if I have a user account in the pipeline with a first name of Dan, PowerShell should be able to create Dan the Machinist with the text

    $_.givenname + "the Machinist"
  5. So, this ought to work:

    get-aduser -filter * -searchbase "ou=machinists,dc=bigfirm,dc=com" | set-aduser -description ($_.givenname + "the Machinist")

     

Although all of my reasoning was sound, it didn't work in PowerShell. Why not?

The one-liners in this column so far have been pretty powerful, because many AD-oriented PowerShell one-liners accomplish what used to require 50 lines of VBScript. PowerShell masks the complexity of decision-making and looping. But it can only take it so far. The basic rule is that whenever you want to use $_ somewhere on the right side of the pipeline (as I've done here), you need to pull out ForEach and a scriptblock. (I’m simplifying, but it’s correct for the AD cmdlets.)

So, what would make it work? Simply re-cast the simple one-liner format into the mildly more complex ForEach-oriented one. Just take my looks-good-but-doesn’t-work one-liner, rebuild it with ForEach and $_, and you get the now-working one-liner

get-aduser -filter * -searchbase "ou=machinists,dc=bigfirm,dc=com" | foreach {set-aduser $_ -description ($_.givenname + "the Machinist")}

Using ForEach doesn’t have to be hard. Just remember that most simple one-liners look like filter, then hammer, as in

get-aduser | unlock-adaccount

To make that kind of construction ForEach-friendly, follow these steps:

  1. Leave the filter (the part to the left of the pipeline) unchanged.
  2. Leave the pipeline in place.
  3. Type foreach { to start the scriptblock.
  4. The hammer cmdlet on the right side of the pipeline almost certainly needs to know the name of the user account that you want it to modify, but in the ForEach world it doesn’t automatically see the object in the pipeline as it does in a simple one-liner. Therefore, present the user account name as $_. What was simply unlock-adaccount must now become unlock-adaccount $_.
  5. Type the remainder of the command, if applicable, and now you’re free to use $_.whatever to extract particular properties from the user account in the pipeline, such as the givenname example in this column. (Oh, and recall that Get-ADUser returns only about 10 of an AD user’s dozens of properties by default. If you want everything, add -properties * to get them all.)
  6. Don’t forget to end the script block with }.

Ready to cook up a ForEach one-liner of your own? I'll provide some more examples next month!