January 12th, 2016

Incorporating Pipelined Input into PowerShell Functions

Summary: Microsoft MVP, Adam Bertram, talks about accepting pipelined input into Windows PowerShell advanced functions.

Microsoft Scripting Guy, Ed Wilson, is here. Today Microsoft MVP, Adam Bertram, returns to talk about accepting pipeline input into advanced Windows PowerShell functions.

Note   This is the second post in a series. Don’t miss Introduction to Advanced PowerShell Functions.

When you begin to learn Windows PowerShell, you'll soon find yourself neck-deep in objects. Objects are one of the fundamental concepts of what makes PowerShell so user-friendly—and powerful at the same time.

As you dive deeper into the language, the next concept that you'll typically stumble over is the pipeline. So let's take your PowerShell skills to the next level by incorporating pipeline input abilities into your advanced functions!

The pipeline is what makes PowerShell so unique. The thought of a pipeline is not new. We've been able to pipe strings from the output of one command to the input of another command for a long time. However, it wasn't until PowerShell came along that we had a language that allowed us to pipe objects from one command to another.

The following is a very basic advanced function called Get-Something with a single parameter called $item. When called, this function simply outputs a sentence to the console that states whatever value of $item is passed to the function:

Function Get-Something {

            [CmdletBinding()]

            Param($item)

            Write-Host "You passed the parameter $item into the function"

}

For a function to accept pipeline input, it needs to be an advanced function. Maybe that's all it needs. I hope!

Image of command output

Nope. Not only did the $item parameter not have a value, but I also got an error message about some input object that cannot be bound to something. I suppose making a function advanced isn't quite the whole story for here. Fear not. The journey required to get us where we need to go is a short one. Read on…

To get the value abc123 to represent the $item variable in our function, PowerShell has to know how to match the value to the $item parameter. This is done through a process called parameter binding. Parameter binding is the selection process that goes on whenever an advanced function or a compiled cmdlet sees something coming from the pipeline. During this process, it tries to match which parameter (the only one in our case) to bind to our abc123 value.

To set up the function for this parameter binding process, I need to smarten the $item parameter a bit by using the Parameter keyword instead of simply throwing $item in between the Param parentheses. This allows me to add parameter attributes to the $item parameter. Among those are two attributes directly related to accepting pipeline input: ValueFromPipeline and ValueFromPipelineByPropertyName.

This will make the function look something like this:

Function Get-Something {

            [CmdletBinding()]

            Param(

                        [Parameter()]

$item

)

            Write-Host "You passed the parameter $item into the function"

}

When the Parameter keyword is representing $item, there is a spot to place the first parameter attribute called ValueFromPipeline:

Function Get-Something {

            [CmdletBinding()]

            Param(

                        [Parameter(ValueFromPipeline)]

$item

)

            Write-Host "You passed the parameter $item into the function"

}

The ValueFromPipeline keyword finally allows passing the abc123 value directly to the Get-Something function:

PS> 'abc123' | Get-Something

PS> You passed the parameter abc123 into the function

This is great! But maybe I want to pass Windows service names to my function and output their names in the sentence:

PS> Get-Service | Get-Something

PS> You passed the parameter System.ServiceProcess.ServiceController into the function

Huh? I was expecting it to look something like this with each service represented by $item:

You passed the parameter Application Identity into the function

You passed the parameter DHCP Client into the function

You passed the parameter DNS Client into the function

Sorry. Work is never done, right? We need to add a little bit more code.

The reason this happens is because the ValueFromPipeline keyword binds entire objects to the parameter. The service name is simply a property of the object type System.ServiceProcess.ServiceController.

When I piped abc123 to Get-Something, that was a simple string. Get-Service is sending a more complicated object to the function now and Get-Something doesn't know how to handle it. I need a way to bind only a single property of the System.ServiceProcess.ServiceController objects that Get-Service outputs.

To do this, I can't use ValueFromPipeline. Instead, I need to use a similar (yet way too long keyword) called ValueFromPipelineByPropertyName in its place:

Function Get-Something {

            [CmdletBinding()]

            Param(

                        [Parameter(ValueFromPipelineByPropertyName)]

$item

)

            Write-Host "You passed the parameter $item into the function"

}

However, if you leave the function as-is and try to pipe Get-Service directly to Get-Something, you'll soon be in for a lot of nasty, red error text.

Image of error message

Why? Because PowerShell still doesn't know how to bind those objects from Get-Service to the function. I need to change the parameter's name to match the object property I'd like to accept input from. In this case, each System.ServiceProcess.ServiceController object that Get-Service outputs has a Name property that I'd like to show up in the sentence output.

Image of command output

Let's change the parameter name to $Name and see what happens:

Function Get-Something {

            [CmdletBinding()]

            Param(

                        [Parameter(ValueFromPipelineByPropertyName)]

$Name

)

            Write-Host "You passed the parameter $Name into the function"

}

PS> Get-Service | Get-Something

PS> You passed the parameter mylastservice into the function

Odd. It only output the Windows service "mylastservice." Why not all of them?

It's because, by default, it will only output the very last object in the pipeline. To process each object output by Get-Service, I need to include the Write-Host line in a process block.

Any code inside a process block is run for each object coming from the pipeline—not only the last one. To process all objects, I'll add one of these process blocks inside of the function:

Function Get-Something {

            [CmdletBinding()]

            Param(

                        [Parameter(ValueFromPipelineByPropertyName)]

$Name

)

process {

            Write-Host "You passed the parameter $Name into the function"

            }

}

PS> Get-Service | Get-Something

PS> You passed the parameter service1 into the function

PS> You passed the parameter service2 into the function

PS> You passed the parameter service3 into the function

PS> You passed the parameter service4 into the function

Much better! You can now see that the name of each service is being successfully passed into the sentence string and being output to the console by Write-Host. You now have a function that you can pass objects or object properties to.

If you got some value from this information and would like to dive deeper into PowerShell, the pipeline, advanced functions, and modules, be sure to check out my Pluralsight course about building advanced PowerShell functions and modules. I'll take you into a way deep dive about advanced functions and modules.

~Adam

Thank you very much for these two awesome posts, Adam. Join me tomorrow for more cool Windows PowerShell stuff. 

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy 

Category
Scripting

Author

Ed Wilson is the former Microsoft Scripting Guy.

0 comments

Discussion are closed.