Tome's Land of IT

IT Notes from the Powertoe – Tome Tanasovski

Tackling the Pipeline with Advanced Functions

While the pipeline is one of the coolest things that exist in Powershell I wish it was named something else.  This is strictly for a selfish reason.  I do not want to feel compelled to use the phrases, “Surf’s Up” or “Cowabunga” within an introduction to an article.  Unfortunately if I’m going to talk about the cool wave of Powershell I’m going to have force you to suck up crappy metaphor with surfing terms.

With that out of the way let’s dive into the internals of passing a [PSObject[]] through the pipeline into a function.  The goal is to mimic the way you can pass multiple lines of CSV text into ConvertFrom-CSV through the pipeline.   In Powershell the pipeline is tied to a parameter of a function.  In our case the parameter will be called InputObject.   Here is the template of what you would start with to do that:

function Get-Pipeline {
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [PSObjects[]]$InputObject
    )
}

If you’re new to Advanced Functions let me help you break it down.  The contents of param() contain the parameter declaration for the function.  The [Parameter] attribute defines the nature of the parameter.  To see the attributes you can use run:

get-help about_Functions_Advanced_Parameters

The arguments I used above for the parameter attribute specify that the parameter is mandatory, it can be used without the parameter name (positional), and it can accept input from the pipeline.  With that in mind take a look at the following script:

function Get-Pipeline {
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [PSObject[]]$InputObject
    )
    $inputobject.count
}

Based on my explanation you probably assume that you could call the function both ways below and expect equal results:

$lines = Get-Content C:\Windows\System32\drivers\etc\hosts
"Calling via parameter:"
Get-Pipeline -InputObject $lines
"Calling via pipeline:"
$lines|Get-Pipeline

Instead what you get is:

Calling via parameter:
21
Calling via pipeline:
1

This tells us that we are not getting an identical object bound to the InputObject parameter like we would expect.

Powershell provides us with special block sections to handle pipeline input in a function: BEGIN, PROCESS, and END.

  • BEGIN gives you a place to initialize any variables or do preprocessing.    The values of the parameters are not accessible in the BEGIN block.
  • END gives you a place to clean up and return finalized objects from your function.
  • PROCESS gives you a way to go through each object if a collection is passed in the pipeline.

When you use the process block you can access each element in turn using the parameter that the pipeline is mapped to.  PROCESS will be called one time for each element of the collection, and it will set $InputObject equal to the current element.  When the blocks are not used in a function Powershell defaults the entire function to the END block.  If you imagine that the PROCESS block is running whether or not you specify it you can see that after the last iteration of PROCESS the $InputObject is set to the last line that is passed into the pipeline.  $InputObject is only a single entry because we are in the END block and the last iteration through the PROCESS block sets $InputObject to the last line.  This is why $InputObject is only a single entry.

If we add the blocks to our script and inspect the $inputobject within PROCESS we can see how the block works via the parameter and via the pipeline:

function Get-Pipeline {
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [PSObject[]]$InputObject
    )
    BEGIN {
    }
    PROCESS {
        $InputObject.count
    }
    END {
    }
}
$lines = Get-Content C:\Windows\System32\drivers\etc\hosts
"Calling via parameter:"
Get-Pipeline -InputObject $lines
"Calling via pipeline:"
$lines|Get-Pipeline

The above returns:

Calling via parameter:
21
Calling via pipeline:
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1

We can now access every line, but it creates a small problem.  We now have to account for whether or not it comes in through the pipeline or if it comes via the parameter of the function.  This defeats the whole point.  Fortunately, there is an easy trick up our sleeve.  $InputObject is always of the type [PSObjects[]].  That [] means that it is always a collection even if there is only 1 element in the collection.  If we modify our PROCESS block to iterate through each element of $InputObject we have given ourselves a way to iterate through each element regardless of how the collection gets passed to the function.  The following demonstrates this as well as a good reason to use the BEGIN block:

function Get-Pipeline {
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [PSObject[]]$InputObject
    )
    BEGIN {
        $i=0
    }
    PROCESS {
        foreach ($object in $InputObject) {
            "Line{0}:{1}" -f $i,$object
            $i++
        }
    }
    END {
    }
}
$lines = Get-Content C:\Windows\System32\drivers\etc\hosts
"Calling via parameter:"
Get-Pipeline -InputObject $lines
"Calling via pipeline:"
$lines|Get-Pipeline

One final note:  There may be times that you want to keep the collection of objects in tact in order to pass collection through a separate pipeline of functions.  This last example shows a technique to do that.  It’s less efficient than the above method because you have to copy the objects into a separate collection rather than just processing the items, but if you ever need it here it is:

function Get-Pipeline {
    param(
        [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
        [PSObject[]]$InputObject
    )
    BEGIN {
        $objects = @()
    }
    PROCESS {
        $objects += $InputObject
    }
    END {
        $objects.count
    }
}
$lines = Get-Content C:\Windows\System32\drivers\etc\hosts
"Calling via parameter:"
Get-Pipeline -InputObject $lines
"Calling via pipeline:"
$lines|Get-Pipeline

I hope this little safari inspires you to start adapting your functions to use the pipeline where you can.  It’s not difficult once you see and understand it, but it is an important technique to adopt to ensure that your cmdlets and functions are as powerful and useful as possible.  COWABUNGA! <- No, I didn’t 😦

Advertisements

One response to “Tackling the Pipeline with Advanced Functions

  1. Pingback: Open a file in Powershell ISE via cmdlet « Tome's Land of IT

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: