The PowerShell pipeline is one of the most important (and useful) features of the PowerShell shell and scripting language. Once you understand the basics of how it works and what it’s capable of, you can leverage its power in your own functions. In this tutorial, you’re going to do just that!
The PowerShell pipeline allows you to chain together commands to build a single ‘pipeline’ which simplifies code, allows parallel processing and more. If you’re ready to learn about the pipeline and to to build your own functions to leverage the pipeline, let’s get started!
Prerequisites
This post will be a tutorial and will be all hands-on demos. If you’d like to follow along, you’ll need PowerShell v3+. This tutorial will be using Windows PowerShell v5.1.
Understanding the PowerShell Pipeline
Most PowerShell commands receive some input via a parameter. The command receives some object as input and does something with it inside. It then optionally returns some object via output.
Commands in a pipeline act like human runners in a relay race. Every runner in the race, except for the first & the last, accepts the baton (objects) from its predecessor and passes it to the next.
For example, the Stop-Service
cmdlet has a parameter called InputObject
. This parameter allows you to pass a specific type of object to Stop-Service
representing the Windows service you’d like to stop.
To use the InputObject
parameter, you could retrieve the service object via Get-Service
and then pass the object to the InputObject
parameter as shown below. This method of providing input to the Stop-Service
cmdlet via the InputObject
parameter works great and gets the job done.
$service = Get-Service -Name 'wuauserv'
Stop-Service -InputObject $service
This method of passing input to the Stop-Service
command requires two distinct steps. PowerShell must run Get-Service
first, save the output to a variable and then pass that value to Stop-Service
via the InputObject
parameter.
Now, contract the above snippet with the below snippet, which does the same thing. It’s a lot simpler because you don’t have to create a $services
variable or even use the InputObject
parameter at all. Instead, PowerShell “knows” you intend to use the InputObject
parameter. It does this via a concept called parameter binding.
You’ve now “chained” commands together with the |
operator. You’ve created a pipeline.
Get-Service -Name 'wuauserv' | Stop-Service
But, you don’t have to use just two commands to create a pipeline; you can chain as many as you want together (if the command parameters support it). For example, the below code snippet:
- Passes all of the objects the
Get-Service
cmdlet returns to theWhere-Object
cmdlet. - The
Where-Object
cmdlet then looks at each object’sStatus
property and then returns only those objects with a value ofRunning
. - Then, each of those objects is sent to
Select-Object
, which only returns theName
andDisplayName
properties of the objects. - Since no other cmdlet accepts the objects that
Select-Object
outputs, the command returns the objects directly to the console.
The
Where-Object
andSelect-Object
cmdlets understand how to process the pipeline input by a concept called parameter binding, discussed in the next section.
Get-Service | Where-Object Status -eq Running | Select-Object Name, DisplayName
For additional information on pipelines, run the command
Get-Help about_pipelines
.
Pipeline Parameter Binding
At first glance, the pipeline may seem trivial. After all, it’s just passing objects from one command to another. But in reality, the pipeline is much more complicated. Commands accept input via parameters only. The pipeline must somehow figure out which parameter to use even when you don’t explicitly define it.
The task of figuring out which parameter to use when a command receives input via the pipeline is known as parameter binding. To successfully bind an object coming in from the pipeline to a parameter, the incoming command’s parameter(s) must support it. Command parameters support pipeline parameter binding in one of two ways; ByValue
and/or ByPropertyName
.
ByValue
The command parameter accepts the entire incoming object as the parameter value. A ByValue
parameter looks for an object of a specific type in incoming objects. If that object type is a match, PowerShell assumes that the object is meant to be bound to that parameter and accepts it.
The
Get-ChildItem
cmdlet has a parameter calledPath
that accepts a string object type and pipeline input viaByValue
. Because of this, running something like'C:\Windows' | Get-ChildItem
returns all files in the C:\Windows directory becauseC:\Windows
is a string.
ByPropertyName
The command parameter doesn’t accept an entire object but a single property of that object. It does this not by looking at the object type but the property name.
The
Get-Process
cmdlet has aName
parameter that’s set up to accept pipeline inputByPropertyName
. When you pass an object with aName
property to theGet-Process
cmdlet like[pscustomobject]@{Name='firefox'} | Get-Process
, PowerShell matches or binds theName
property on the incoming object to theName
parameter and uses that value.
Discovering Command Parameters that Support the Pipeline
As mentioned earlier, not every command supports pipeline input. The command author must create that functionality in development. The command must have at least one parameter that supports the pipeline, put ByValue
or ByPropertyName
.
How do you know which commands and their parameters support pipeline input? You could just try it with trial and error but there’s a better way with the PowerShell help system using the Get-Help
command.
Get-Help <COMMAND> -Parameter <PARAMETER>
For example, take a look at the Get-ChildItem
cmdlet’s parameter Path
below. You can see that it supports both types of pipeline input.
Once you know which command parameters support pipeline input, you can then take advantage of that functionality, as shown below.
# none-pipeline call
Get-ChildItem -Path 'C:\\Program Files', 'C:\\Windows'
# pipeline call
'C:\\Program Files', 'C:\\Windows' | Get-ChildItem
But, on the other hand, the DisplayName
parameter on Get-Service
does not support pipeline input.
Building Your Own Pipeline Function
Even though the standard PowerShell cmdlets support pipeline input doesn’t mean you can’t take advantage of that functionality. Thankfully, you can build functions that accept pipeline input too.
To demonstrate, let’s start with an existing function called Get-ConnectionStatus
.
- This function has a single parameter (that does not accept pipeline input) called
ComputerName
, which allows you to pass one or more strings to it. You can tell theComputerName
parameter doesn’t accept pipeline input because it’s not defined as a parameter attribute ([Parameter()]
). - The function then reads each of those strings and runs the
Test-Connection
cmdlet against each one. - For each string computer name passed, it then returns an object with a
ComputerName
andStatus
property.
function Get-ConnectionStatus
{
[CmdletBinding()]
param
(
[Parameter()] ## no pipeline input
[string[]]$ComputerName
)
foreach($c in $ComputerName)
{
if(Test-Connection -ComputerName $c -Quiet -Count 1)
{
$status = 'Ok'
}
else
{
$status = 'No Connection'
}
[pscustomobject]@{
ComputerName = $c
Status = $status
}
}
}
You’d then invoke the Get-ConnectionStatus
by passing the ComputerName
parameter one or more hostnames or IP addresses like below.
Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'
Step 1: Allowing Pipeline Input
To get this function to accept pipeline input, you must first define the appropriate parameter attribute. This attribute can either be ValueFromPipeline
to accept pipeline input ByValue or ValueFromPipelineByPropertyName
to accept pipeline input ByPropertyName.
For this example, add the ValueFromPipeline
parameter attribute within the brackets of the [Parameter()]
definition as shown below.
[Parameter(ValueFromPipeline)]
[string[]]$ComputerName
At this point, that’s technically all you have to do. The Get-ConnectionStatus
function will now bind any string object passed to it to the ComputerName
parameter. But, even though parameter binding is taking place doesn’t mean the function will do anything meaningful with it.
Step 2: Adding a Process Block
When you want PowerShell to process all objects coming in from the pipeline, you must next add a Process
block. This block tells PowerShell to process each object coming in from the pipeline.
Without a
Process
block, PowerShell will only process the first object coming from the pipeline. TheProcess
block tells PowerShell to continue processing objects.
Add a Process
block as shown below by enclosing all of the functionality of the function.
function Get-ConnectionStatus
{
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline)]
[string[]]$ComputerName
)
Process ## New Process block
{
foreach($c in $ComputerName)
{
if(Test-Connection -ComputerName $c -Quiet -Count 1)
{
$status = 'Ok'
}
else
{
$status = 'No Connection'
}
[pscustomobject]@{
ComputerName = $c
Status = $status
}
}
} ## end Process block
}
Always send output from within the
Process
block. Sending output from within theProcess
block “streams” the objects out to the pipeline allowing other commands to accept those objects from the pipeline.
Passing Objects to the PowerShell Pipeline
Once you have a Process
block defined in the function above, you can now call the function passing values to the ComputerName
parameter via the pipeline as shown below.
Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'
## or
'127.0.0.1', '192.168.1.100' | Get-ConnectionStatus
At this point, you can leverage the real power of the pipeline and begin incorporating more commands into the mix. For example, perhaps you have a text file, C:\Test\computers.txt, with a line of IP addresses separated via a new line like below.
127.0.0.1
192.168.1.100
You could then use the Get-Content
cmdlet to read each of those IP addresses in the text file and pass them directly to the Get-ConnectionStatus
function.
Get-Content -Path C:\Test\computers.txt | Get-ConnectionStatus
Taking this setup one step farther, you could pipe the objects that Get-ConnectionStatus
returns directly to the ForEach-Object
cmdlet.
The code below:
- Reads all computer names in the text file and passes them to the
Get-ConnectionStatus
function. Get-ConnectionStatus
processes each computer name and returns an object with the propertiesComputerName
andStatus
.Get-ConnectionStatus
then passes each object to theForEach-Object
cmdlet, which then returns a single string colored as Cyan with a human-readable status.
Get-Content -Path C:\Test\computers.txt |
Get-ConnectionStatus |
ForEach-Object { Write-Host "$($_.ComputerName) connection status is: $($_.Status)" -ForegroundColor Cyan }
If pipeline input wasn’t enabled on the
ComputerName
parameter or ifGet-ConnectionStatus
did not return an object within theProcess
block, PowerShell would not return any status to the console until all objects (IP addresses) are processed.
Pipeline Binding by Property Name
Until now, the Get-ConnectionStatus
cmdlet is set up to accept pipeline input ByValue (ValueFromPipeline
) by accepting an array of strings like '127.0.0.1', '192.168.1.100'
. Would this function also work as expected if the input was received from a CSV file vs. a text file of IP addresses?
Perhaps you have a CSV file that looks like below at C:\Test\pc-list.csv.
ComputerName,Location
127.0.0.1,London
192.168.1.100,Paris
Note that the
ComputerName
field in the CSV file is the same name as theGet-ConnnectionStatus
ComputerName
parameter.
If you’d attempt to import the CSV and pass it over the pipeline to Get-ConnectionStatus
, the function returns an unexpected result in the ComputerName
column.
Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus
ComputerName Status
------------ ------
@{ComputerName=127.0.0.1; Location=London} No Connection
@{ComputerName=192.168.1.100; Location=Paris} No Connection
Can you guess what went wrong? After all, the parameter name does match, so why didn’t the PowerShell pipeline bind the output that Import-CSV
returned to the ComputerName
parameter on Get-ConnectionStatus
? Because you need the parameter attribute ValueFromPipelineByPropertyName
.
As of right now, the function’s ComputerName
parameter has a parameter definition that looks like [Parameter(ValueFromPipeline)]
. Therefore, you must add ValueFromPipelineByPropertyName
to set the ComputerName
parameter to support input ByPropertyName, as shown below.
[CmdletBinding()]
param
(
[Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
[string[]]$ComputerName
)
Once you had pipeline support ByPropertyName, you tell PowerShell to begin looking at the object property names and the object type. Once you’ve made this change, you should then see the expected output.
PS> Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus
ComputerName Status
------------ ------
127.0.0.1 Ok
192.168.1.100 No Connection
Summary
In this tutorial, you learned how the PowerShell pipeline works, how it binds parameters, and even how to create your own function supporting the PowerShell pipeline.
Even though functions will work without the pipeline, they will not “stream” objects from one command to another and simplify code.
Can you think of a function you wrote, or maybe about to write, that can benefit from making it pipeline ready?