From One-Liner to ForEach One-Liner

From One-Liner to ForEach One-Liner

Get ready for better Active Directory PowerShell tools

In "Where-Object and the Pipeline" and "Introducing the Pipeline and ForEach," I discussed the two essential building blocks for one-liners: the pipeline and ForEach. This month, I want to show you how to put them together to start building some of those one-liners.

So far, this column's one-liners have looked like this one, which finds everyone who hasn’t logged on in the past 90 days and disables their accounts:

search-adaccount -UsersOnly -AccountInactive -TimeSpan "90" | disable-adaccount

The first cmdlet, search-adaccount, finds the delinquent users and passes them to the second cmdlet, disable-adaccount, which disables the accounts; the pipeline performs the passing. The fact that PowerShell lets you do something so useful with such a simple line of text is wonderful because it masks a fair amount of complexity: no looping commands, no variables, and a minimum of punctuation. Unfortunately, if you want to go beyond that fairly basic one-liner and squeeze more automation power out of PowerShell, you need to take a step back. You need to consider how you might have accomplished that disable-inactive-accounts task in a way that’s uglier, yes, but that ultimately proves far more flexible.

In particular, consider the second cmdlet, disable-adaccount. Typically, you'd have to invoke it with the name of the account that you want to disable. Open a PowerShell prompt, type just disable-adaccount, and press Enter, and PowerShell will complain that you haven’t entered an “identity.” Add a logon name like this, however, and it would run without error:

disable-adaccount JFrost

In the one-liner at the start of this column, however, no name was offered to disable-adaccount, and it didn’t complain; that's because the cmdlet’s author built some intelligence into it. If it’s invoked without an account name, it looks in the pipeline to see if it can find anything that looks like an account name, a SID, an object GUID, or a distinguished name (DN). As the search-adaccount cmdlet emits entire user objects—all of which have a name, DN, SID, and GUID—a disable-adaccount on the receiving end of a pipeline has more than enough information to identify the account to disable. Again, though, it’ll accept less, as you can see by just stuffing a samaccountname or a SID into the pipeline and invoking disable-adaccount on the other side, like this:

"S-1-5-21-3471743624-104184042-3101405554-3104" | disable-adaccount

or this:

"jdorn" | disable-adaccount

In both cases, I've merely shoved some text (a string, in programming terminology) into the pipeline. PowerShell then wakes up disable-adaccount and drops the pipeline contents into disable-adaccount’s lap, and then disable-adaccount tries to match the pipeline contents to any known logon names, SIDs, GUIDs, or DNs. If there’s a match, great; if not, disable-adaccount just displays an error, as it would if I’d typed

"roses are red" | disable-adaccount

That’s not going to match anything that looks like an AD account identifier. Fortunately, most Active Directory (AD)-related cmdlets work the same way, and so doing the same thing with remove-aduser, unlock-adaccount, enable-adaccount, and get-aduser would succeed as well. (Think twice about experimenting with remove-aduser, though!)

Now, let’s reconstruct that one-liner, but this time we’ll employ ForEach and the variable that contains the pipeline’s current contents, $_. As you’ve seen, to use ForEach, you first fill the pipeline, then insert the pipeline (|) and a foreach, and then put some commands between the caret brackets—or, in PowerShell terms, a scriptblock. Thus, our one-liner in ForEach terms would look something like

search-adaccount -UsersOnly -AccountInactive -TimeSpan "90" | foreach {disable-adaccount}

That won’t work, though, because disable-adaccount doesn’t automatically get pipeline input when invoked in a ForEach scriptblock, so you have to explicitly tell disable-adaccount which account to disable. Otherwise, it’ll stop dead as it would if you just typed disable-adaccount all by itself on a command line. This small change will satisfy disable-adaccount:

search-adaccount -UsersOnly -AccountInactive -TimeSpan "90" | foreach {disable-adaccount $_ }

The $_ variable satisfies disable-adaccount’s need for an account to disable, because at that moment the pipeline contains an AD user object. Where’s the percentage in the more complex syntax? More opportunity. For example, that one-liner produces no output at all unless there’s an error, so what if you want a log of the accounts you just deleted? A cmdlet named add-content will write a line of text to an existing file if it's followed by a filename and some text, as in

add-content c:\logs\disable.log "Disabled another one!"

A semicolon lets you put more than one command in a scriptblock, so this modification would log a line for each disabled user’s logon name, which is stored in $_.samaccountname:

search-adaccount -UsersOnly -AccountInactive -TimeSpan "90" | foreach {disable-adaccount $_; add-content c:\logs\disable.log $_.samaccountname}

Now we’re getting closer to building some pretty neat stuff. See you next month for more!

Hide comments


  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.