PowerShell Pipeline
Building PowerShell Functions That Support the Pipeline
Here's how to build some universal functions that will survive your environment.
If you've been using PowerShell, then you know that one of the great things about it is the ability to take objects outputted from one cmdlet, easily send it to another cmdlet and have it know how to handle the object. A great example of this is using Get-Process and taking one (or more) objects that are outputted and send that into Stop-Process to end the processes.
Get-Process -Name powershell | Stop-Process -WhatIf
In this case, I didn't actually stop the process, but you can see how easy it was to send the object from one cmdlet to another with no effort at all. We can use Trace-Command to figure out that the InputObject parameter is accepting the incoming object using ValueFromPipeline attribute.
Trace-Command -Name ParameterBinding -Expression {
Get-Process -Name powershell | Stop-Process -WhatIf
} -PSHost
We can also use Get-Help to show that InputObejct does in have ValueFromPipeline support enabled.
Get-Help Stop-Process -Parameter InputObject
ValueFrom* Attributes
I've already mentioned the ValueFromPipeline attribute that will take an object by its type and attempt to bind to a parameter that supports it. It will first try to match up with the object type and if that doesn't work, it will then try to coerce the type by casting it as that object it is expecting. If that doesn't work, then it will throw an error.
Another attribute is ValueFromPipelineByPropertyName, which means that if the incoming object has a property name that is equal to the parameter name with this attribute enabled, then the object will be processed using that object property type, if applicable. Otherwise it will try to coerce the type into whatever the parameter type is expecting. If that cannot happen, then an error will be thrown.
So what is the order of processing for these attributes? Glad you asked that question!
Order of Parameter Binding Process from Pipeline:
- Bind parameter by Value with same Type (No Coercion)
- Bind parameter by PropertyName with same Type (No Coercion)
- Bind parameter by Value with type conversion (Coercion)
- Bind parameter by PropertyName with type conversion (Coercion)
With that out of the way, we can take a dive into writing a couple of functions that support objects coming from the pipeline. Something that is very important to know when building functions that support objects from pipeline is that you must have a Process block in your function! This is where the items coming from the pipeline are evaluated. By excluding this block, everything will be treated like it is an End statement and only show you the last item coming in from the pipeline.
ValueFromPipeline
Let's start off with a function to perform some tests.
#region Test Function
Function Test-Object {
[cmdletbinding()]
Param (
[parameter(ValueFromPipeline)]
[int[]]$Integer
)
Process {
$_
}
}
#endregion Test Function
This function only accepts integers from the pipeline and outputs the object. I chose an integer because it will help demonstrate a couple examples when I pass some good and bad data into the function.
The first test will be the easiest as the data will work just like I want it to by passing an integer to the function.
$Object = [int]5
$Object | Test-Object
This worked because the parameter type is an integer which is exactly what the function was looking for. To better show this, I am going to run a trace against this same command using Trace-Command and looking at the parameterbinding to see what happens.
Trace-Command -Name ParameterBinding -Expression {
$Object | Test-Object
} -PSHost
Here we can see that the binding attempts to verify that the value coming in is actually an integer and since it is an integer, it accepts the data and binds it to the Integer parameter.
From here on out, all of my commands will include the trace output to better explain the results that you may run into and how the parameter binding looks at the data being brought into the function via the pipeline.
With that, we will now look at how the function handles a string representation of an integer.
Trace-Command -Name ParameterBinding -Expression {
'5' | Test-Object
} -PSHost
Here we see that the string data is skipped during the attempt to bind by data type and then is accepted to be bound to the parameter after a successful type conversion from its string type.
The last example will fail by design by passing data that will not only be something other than an integer, but also cannot be converted to an integer.
$Object = [pscustomobject]@{
Int = '5'
Name = 'test'
}
Trace-Command -Name ParameterBinding -Expression {
$Object | Test-Object
} -PSHost
We can see from Figure 7 that both attempts to bind the data (a PSObject) to the Integer parameter because it is not an integer and it also cannot be converted to an integer.
ValueFromPipelineByPropertyName
As with what we looked at with the ValueFromPipeline attribute, we now will take a look at how a function handles data coming into the function that has the same property name as the parameter.
Function Test-Object {
[cmdletbinding()]
Param (
[parameter(ValueFromPipelineByPropertyName)]
[int[]]$Integer
)
Process {
$_
}
}
$Object = [pscustomobject]@{
Integer = '5'
Name = 'test'
}
Trace-Command -Name ParameterBinding -Expression {
$Object | Test-Object
} -PSHost
Here the property does match the parameter name, so ValueFromPipelineByPropertyName is able to pull the data, but the attempt to match it up with the integer type fails so this binding is skipped. However, the second shot at binding succeeds because the binding was able to convert the string value of "5" to its integer type.
If the value of 5 was actually an integer already, then it would have passed the first attempt at binding the data to the parameter as both types would be an integer. If I would have put something such as 'test' in the Integer property, both attempts to bind the data by property name would have failed because the first attempt would have seen that the data is not an integer and the attempt to convert a non-integer string to an integer would fail as well.
Using Both Attributes in a Function
Wrapping this up, I will show a single example that steps through all of the attempts to bind the parameter that we have covered earlier and ends up successfully binding the data to the parameter with the last attempt.
#region Test Function
Function Test-Object {
[cmdletbinding()]
Param (
[parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)]
[int[]]$Integer
)
Process {
$_
}
}
#endregion Test Function
#region Successful Test Going through ALL VALIDATIONS
$Object = [pscustomobject]@{
Integer = '5'
Name = 'test'
}
Trace-Command -Name ParameterBinding -Expression {
$Object | Test-Object
} -PSHost
#endregion Successful Test Going through ALL VALIDATIONS
We see that each attempt to bind the data to the parameter fails either because it doesn't match the parameter type or it cannot convert the data being piped to an integer. It is only by taking the property name that matches the parameter and then converting the value to an integer that we are able to successfully bind the data to the parameter.
With this knowledge of how PowerShell handles data coming from the pipeline, you can now build some powerful functions that can make not only your work easier, but also those who use your functions as well!
About the Author
Boe Prox is a Microsoft MVP in Windows PowerShell and a Senior Windows System Administrator. He has worked in the IT field since 2003, and he supports a variety of different platforms. He is a contributing author in PowerShell Deep Dives with chapters about WSUS and TCP communication. He is a moderator on the Hey, Scripting Guy! forum, and he has been a judge for the Scripting Games. He has presented talks on the topics of WSUS and PowerShell as well as runspaces to PowerShell user groups. He is an Honorary Scripting Guy, and he has submitted a number of posts as a to Microsoft's Hey, Scripting Guy! He also has a number of open source projects available on Codeplex and GitHub. His personal blog is at http://learn-powershell.net.