Troubleshooting Pipeline Parameter Binding by Peeking Inside

Over at Windows IT Pro, I recently posted an article on how to use Trace-Command to troubleshoot pipeline parameter binding. Several folks asked for a more detailed tutorial... so here ya go.

Why doesn't this command work?

get-adcomputer -filter * |
select @{n='computername';e={$}} |
invoke-command -ScriptBlock { dir }

Well, let's perhaps start with the obvious. My best troubleshooting technique is to "back off a command," so I'll remove the last command and just try this first two:

OK, that works. It creates an object that has a computername property. I've looked at the help for Invoke-Command, and it says the -computerName parameter accepts pipeline input ByPropertyName. So, if I pipe it an object that has a computerName property - which I've created - it should bind the computer names to the -computerName parameter, thus invoking my "dir" command on those computers. Except that when I try it...

Well crud.

Time for Trace-Command! I'm going to take my entire dysfunctional command-line, bury it in a trace, and see what happens.

The output from this can be pretty tricky, so let's walk through it. Before we do, notice that the command I'm invoking is pretty harmless. You need to be careful when tracing, because your commands actually do run. If you just want to test, remember that most potentially-harmful commands include a -whatif parameter. So, you can include -whatif on the last command in your command-line. For super-complex, lengthy commands, you may want to break things down into manageable hunks so that you can just focus on one piece at a time.

First hunk of output:

You're seeing the parameter binding for the first cmdlet in my expression, Get-ADComputer. You're seeing it bind my parameter value, "*," to the -Filter parameter. It tries to coerce my value to a String, realizes it doesn't have to, and proceeds. You can see that it's binding this positionally, since I didn't actually type the computer name. It does a quick check to see if there are any unspecified mandatory parameters (which there aren't), and it moves on.

Next up is my Select-Object cmdlet. Now, keep in mind that at this point no commands have run. It's just binding the parameters that I've typed

Again, we're looking at a positional parameter, since I didn't type the -Property parameter. It's binding that hash table (the bit that starts with @{ and ends with }}) to the -Property parameter. The -Property parameter actually requires an array, and I've only provided a single item, so PowerShell creates an array and sticks my one item into it. A mandatory parameter check is passed, and we're off and running.

There's the last cmdlet in my expression, Invoke-Command. The script block containing "did" is bound to the -ScriptBlock parameter - no positional binding here, since I actually bothered to type the parameter name this time. A quick check for positional parameters (I didn't provide any), a mandatory parameter check, and we're done.

Having bound all of the parameters I typed, PowerShell can now begin executing the pipeline.

What you're seeing here is the Get-ADComputer cmdlet running. It puts one object at a time into the pipeline (in my case, there's only one object in the domain for it to work with, so we'll only run through this sequence one time). PowerShell now knows that there's an object in the pipeline, so it starts to "BIND PIPELINE object to parameters: [Select-Object]." In other words, "ok, there's an object being sent to Select-Object... WTF do I do with it?!?!"

PowerShell sees that the type of object in the pipeline is a Microsoft.ActiveDirectory.Management.ADComputer - or just ADComputer for short. It also sees that Select-Object is able to accept any kind of SystemObject on its -InputObject parameter. That's kind of like having AB-positive blood: You can accept any type you want. So my ADComputer object is bound to the -InputObject parameter. Done.

Now PowerShell begins executing Invoke-Command, and sees that there's a command in that thar script block: Dir. Technically, that's Get-ChildItem. So it has to do a quick binding on any parameters I've typed:

Since I didn't type any parameters for Dir, this finishes pretty quickly.

Now PowerShell can begin executing Invoke-Command in earnest:

WAIT, WAIT, WAIT, WAIT, WAIT. I specifically said that I expected the output of Select-Object to bind to the -computerName parameter of Invoke-Command. That isn't what happened.

As an aside: You can't do this type of troubleshooting if you don't have an expectation. Troubleshooting is about thinking something is happening one way, and then figuring out where its behavior actually deviates from that expectation. No expectation, no troubleshooting. So you have to know how you think something is working, and then see if you're right or not.

Here, I'm seeing PowerShell binding my Selected.Microsoft.ActiveDirectory.Management.ADComputer object to the -InputObject parameter of Invoke-Command. What the what?

Looking at the help for Invoke-Command, I think I see the problem:

The -InputObject parameter binds pipeline input ByValue, and it accepts input of the type PSObject. In the world of pipeline binding, ByValue happens FIRST. I was relying on that failing, and ByPropertyName kicking in, so that my ComputerName property got bound to the -computerName parameter. Didn't happen. -InputObject is one of those AB-positive blood type parameters - it takes anything. So it snatched up my ADComputer object, and according to the help, it's passing my computer object to whatever's inside the script block. In other words, my computer object is being piped to Get-ChildItem on the remote computer. Rats.

All of a sudden, my error message makes sense:

Get-ChildItem is telling me "Dude, I can't give you a directory listing for the path [your computer object] because there is no such path." It basically converted some of the properties of my selected computer object - which only contains a ComputerName property - and tried to get a directory listing for a path of that name. 

Jeffrey Snover once told me, "if you've got a parameter that accepts Object (or PSObject) from the pipeline ByValue, then no other parameter binding will take place. It will grab everything." Now, there may be some more devious detail under the hood, but that describes exactly what we're seeing. In fact, in my head, I almost regard Invoke-Command as having a buggy design. The help file states that other parameters are rigged up to accept pipeline input, when in fact they cannot do so because -InputObject is always going to snatch whatever comes down the pipe.

Anyway, regardless of whether or not Invoke-Command should or shouldn't behave this way, we can clearly see - thanks to Trace-Command - that it did behave this way. And so I simply rewrote my command to not rely on the pipeline:

invoke-command -ScriptBlock { dir } -computername (get-adcomputer -filter * | select -expand name)

Posted Jul 31 2012, 05:53 PM by Don Jones
Copyright 2012 All rights reserved.