Getting to Know the PowerShell Pipeline and Creating Functions

Published:4 May 2021 - 8 min. read

Azure Cloud Labs: these FREE, on‑demand Azure Cloud Labs will get you into a real‑world environment and account, walking you through step‑by‑step how to best protect, secure, and recover Azure data.

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:

  1. Passes all of the objects the Get-Service cmdlet returns to the Where-Object cmdlet.
  2. The Where-Object cmdlet then looks at each object’s Status property and then returns only those objects with a value of Running.
  3. Then, each of those objects is sent to Select-Object, which only returns the Name and DisplayName properties of the objects.
  4. Since no other cmdlet accepts the objects that Select-Object outputs, the command returns the objects directly to the console.

The Where-Object and Select-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 called Path that accepts a string object type and pipeline input via ByValue. Because of this, running something like 'C:\Windows' | Get-ChildItem returns all files in the C:\Windows directory because C:\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 a Name parameter that’s set up to accept pipeline input ByPropertyName. When you pass an object with a Name property to the Get-Process cmdlet like [pscustomobject]@{Name='firefox'} | Get-Process, PowerShell matches or binds the Name property on the incoming object to the Name 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.

PowerShell pipeline input is allowed
PowerShell pipeline input is allowed

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.

PowerShell pipeline input is not allowed
PowerShell pipeline input is not allowed

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 the ComputerName 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 and Status 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. The Process 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 the Process 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 properties ComputerName and Status.
  • Get-ConnectionStatus then passes each object to the ForEach-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 if Get-ConnectionStatus did not return an object within the Process 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 the Get-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?

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!