As introduced in my previous post, this is part one of a four part series that documents some things I discovered or had clarified by reading the excellent Learn PowerShell in a Month of Lunches book (recently released in 3rd edition).

This post covers some things I learnt about the PowerShell pipeline, which include:

  • How pipeline input knows where to go
  • If you can't get input to a property via the pipeline there's another way
  • the concept of 'one script one pipeline'

How pipeline input knows where to go

Like 'how does something as heavy as an aeroplane stay in the sky?', pipeline input was something I was glad worked but tried not to think about too deeply. In fact it hadn't really seemed important, certain (similar named) cmdlets obviously work together (get-service 'myservice' | stop-service), it didn't seem to matter how. And then where it was less obvious how cmdlets might go together I had somehow learnt getting pipeline input in to a cmdlet was a matter of matching the relevant output and input types.

The (more in depth) answer is apparently, one of two ways:

1. By value (ByValue). A cmdlet may have a single parameter of any particular type that accepts pipeline input by value. That is to say (for example) if there are three properties that accept string input, only one of them may accept pipeline input ByValue (but a single property of each different type can also accept input ByValue). When you send pipeline input to the cmdlet, if the input is of a matching type then that's the parameter it is bound to.

You can see if any parameters accept input ByValue in get-help -full

For example Stop-Process has the following parameter:

-InputObject <Process[]>
    Specifies the process objects to stop.

    Accept pipeline input?       True (ByValue)

The parameter -InputObject expects one or more Process objects. The get-process cmdlet creates Process objects (as you can see via get-process | get-member):

   TypeName: System.Diagnostics.Process

As as such, when piped they are bound to the -InputObject parameter of Stop-Process.

2. By name (ByPropertyName). If (and only if) there is no property that matches ByValue, then it will attempt to match any properties that accept pipeline input by property name, again as seen in get-help -full.

For example, get-service has the following parameters:

-ComputerName <String[]>
    Gets the services running on the specified computers.

    Accept pipeline input?       True (ByPropertyName)

-Name <String[]>
    Specifies the service names of services to be retrieved. 

    Accept pipeline input?       True (ByPropertyName, ByValue)

Note that both of these have the same type. That doesn't matter, because here it knows where to bind different data based on the property names of the input object. For example, if I have a CSV file that has both ComputerName and Name columns and then did import-csv myfile.csv | get-service the contents of my CSV file would be bound to the relevant parameters.

Notice that in the previous example -Name accepts input by both value and property name. ByValue comes first, so if I just pipeline input a string, e.g "spooler" | get-service then that input is going to be bound to the -name parameter only. In my CSV example, the "value" (type) of the pipeline input is a PSCustomObject (which is what is generated by Import-CSV) and as a result there is no ByValue match, and so it falls back to ByPropertyName and maps together every matching property name it has.

If you can't get input to a property via the pipeline there's another way

In the previous example, you can see that in get-service both -name and -computername accept pipeline input ByPropertyName and -name accepts input ByValue. So what if I have a list of strings and want to pipe them explicitly in to the -computername parameter? Well the short answer is you can't, but Month of Lunches suggested an alternative.

In the past, I might have done this with a ForEach-Object loop, say something like this:

$ Servers = Get-Content 'servers.txt'
$ Servers | ForEach-Object {
    Get-Service -ComputerName $_

The better/shorter option is this:

Get-Service -ComputerName (Get-Content 'servers.txt')  

Here the brackets work much like in mathematics and result in their contents being executed first. Because -ComputerName accepts array (multiple) input, the Get-Content cmdlet creates an array of strings which are then sent to the -ComputerName parameter and the Get-Service cmdlet processes each. Note this only works in this way because -ComputerName accepts array input. If it didn't, i'd definitely have to fall back to a loop construct.

The concept of brackets forcing execution first was not new to me (e.g I'd used it plenty via subexpressions in strings) but I don't think i'd ever considered using it in this way to create more efficient/tidy/cool code.

One script one pipeline

This is something else that i'd never considered before. Most interestingly, it's a key difference between how commands execute when entered in the console compared to how they execute when you run a script.

When you run cmdlets sequentially at the console, each one executes in its own pipeline (as soon as you press enter):

Get-Service <enter>  

With the above you get two separate outputs, each formatted correctly for their different types.

But if you put those commands in a script and execute it, you'll get strange output when it gets to the Get-Process cmdlet as essentially both execute within the same pipeline and so get piped together to a single formatting cmdlet (the one for Get-Service as it was first).

Interestingly, you can cause the same issue at the console by entering Get-Service; Get-Process and hitting enter at the console.

The point is, when writing scripts/functions it's important (for this reason as well as others) to only return one kind of result.

This concludes part one. In part two I cover some things I learnt about the PowerShell help system get-help.