March 27th, 2018

Doing More With Functions: Taking Parameters on the Pipe

Kory Thacher
Premier Field Engineer

In an earlier post, I showed you how you could use the [parameter(mandatory)] attribute to force your parameters to behaveĀ  a bit more like you’d expect from other languages. We also have a bunch of other useful attributes we can use on our parameters to enable cool features.

Pipelineing

The pipe might feel pretty magical to you in PowerShell, as it just seems to work with our built in cmdlets. You can add this same kind of functionality to your own tools, and we give you two options to do so.

First of all, here is what happens if you don’t use this attribute:

function PipeValueTest
{
param($p1) #no pipe specified
Write-host "$p1 was received" -ForegroundColor Green
}
"hello" | PipeValueTest #pipe will fail to grab data
was received

Pipeline By Value

When we talk about stuff coming in “by value” we mean that you told PowerShell some specific parameters that can take data off the pipe if we are given values matching their type (or convertible to their type, which can cause some weird behavior). We can do this using the “ValueFromPipeline” option in our [parameter()] attribute.

Let’s see a simple example in action and then see some of the things we need to be aware of:

function PipeValueTest
{
param([parameter(ValueFromPipeline)]$p1) #no data type to care out
Write-host "$p1 was recieved" -ForegroundColor Green
}

PipeValueTest -p1 "hi" #we can still use it normally
"hello" | PipeValueTest #now we can pipe it

Let’s try adding a data type and seeing how this behaves as well:

function PipeValueTest
{
param([parameter(ValueFromPipeline)][int]$p1) #integers only
Write-host "$p1 was recieved" -ForegroundColor Green
}

1 | PipeValueTest #pipe an int
"5" | PipeValueTest #pipe a string we can convert to int
"frank" | PipeValueTest #pipe a data type we aren't expecting and can't convert

Notice that PowerShell has a conversion step, where it tried to convert what it saw on the pipe to what we were asking for. This step is generally there for convenience, but occasionally could cause problems if it converts data in a way you didn’t intend. When we gave it an easy string “5” we were all set, but when we gave it “frank” it let us know it had no idea what to do with a string on the pipe that it couldn’t convert. (0 is the default value for an undefined integer in PowerShell if you’re wondering why we got that back after the error).

This conversion step can cause some odd problems with multiple parameters that use pretty basic types like string or int:

function PipeValueTest
{
param([parameter(ValueFromPipeline)][int]$intP,
[parameter(ValueFromPipeline)][string]$stringP)
Write-host "Int: $intP was recieved" -ForegroundColor Green
Write-host "String: $stringP was recieved" -ForegroundColor magenta
}

1 | PipeValueTest #pipe an int -- uh oh conversion!
"5" | PipeValueTest #pipe a string we can convert to int -- uh oh
"frank" | PipeValueTest #pipe a string we can't convert to an int

So the moral of the story is that when you’re using “By Value” you need to be careful and test. Usually you just flag your default parameter to take in stuff by value for each parameter set to avoid these situations. We do have another option for receiving data on the pipe, which gives us a lot of flexibility.

Pipeline By Property Names

If we ever receive something of a type we didn’t flag as “by value”, that fails the conversion step, we can actually check if the object recieved has properties that match our parameters. If the object property and parameter data type match, it takes it, or tries to convert it before ignoring it/erroring. A cool way to see this already happening is to run the following:

get-service ALG | get-process #this will error

Now that line of code doesn’t make any sense, but the error is the interesting part:

get-process : Cannot find a process with the name “ALG”. Verify the process name and call the cmdlet again.

How did get-process know to look for a process named ALG? Get-Process doesn’t have a single parameter expecting a service object. Well, our service objects have a “Name” property, and our Get-Process cmdlet has a -name parameter. Let’s take a look at the help for it.

Notice that it shows “Accept pipeline input? TRUE (ByPropertyName).

  1. It recieved a service object it wasn’t expecting
  2. It failed conversion to something it was expecting
  3. It checked the parameters flagged “ByPropertyName” against the service object properties and sucked out the name “Alg”
  4. if “ALG” weren’t a string it would have tried to convert it

Let’s add this to our own code, so we can take in some custom objects on the pipe.

function PipePropertyTest
{
param([parameter(ValueFromPipelineByPropertyName)][int]$intP,
[parameter(ValueFromPipelineByPropertyName)][string]$stringP)
Write-host "Int: $intP was recieved" -ForegroundColor Green
Write-host "String: $stringP was recieved" -ForegroundColor magenta
}

$obj = [pscustomobject]@{
intP = 5
stringP = "hello"
testP = "test"
}

$obj | PipePropertyTest

This is really neat because it let’s you make a suite of tools that could all output a custom object type and then you can pipe them into each other to get the same kind of “magic” functionality that our tools have (IE get-service, stop-service, restart-service, etc). It also gives you the really nice ability to just read in a CSV with import-csv, then pipe the results into your code as long as you have your parameters matching the headers. I’ll do a series of posts in the future about working with different data types if you’re unfamiliar with this action.

Aliases

Just like we can assign aliases to cmdlets, we can assign aliases for our individual parameters. This actually already exists in some places:

get-process FAKE1
get-process FAKE2 -ErrorAction SilentlyContinue
get-process FAKE3 -EA SilentlyContinue #-ea is an alias for -erroraction

To assign alises to your own parameters, we use another attribute with the nice friendly name [alias()] and we simply list out all the alternate names we want inside of those parens:

function SimpleAliasParam
{
param([alias("t")]$a)
"I got $a"
}

SimpleAliasParam -a "hi"
SimpleAliasParam -t "look the alias works"

Why is this cool?

  1. If someone is using your tool already, but you want to change the name of a param (maybe to match some standard you found) then it lets you do so without breaking their code.
  2. Taking in values on the pipe by property name can become very flexible.

Maybe you have a function that was used to read in a CSV from HR and generate a ton of AD users. You named your parameters after the headers you were given. Everything worked, you made the accounts, etc. 6 months later you need to use it again, but this time Bob from HR sent you a spreadsheet with all different headers. Instead of changing your param names, you can alias them so that either spreadsheet format will work in the future.

$obj = [pscustomobject]@{ #pretend this comes in from import-csv
Dimension = "c137"
First = "Rick"
Last = "Sanchez"
}

$obj2 = [pscustomobject]@{ #pretend this comes in from import-csv
Location = "c137"
FirstName = "Beth"
LastName = "Smith"
}

function PipePropertyTest
{
param([parameter(ValueFromPipelineByPropertyName)][string]$First,
[parameter(ValueFromPipelineByPropertyName)][string]$Last,
[parameter(ValueFromPipelineByPropertyName)][string]$Dimension)
Write-host "Fist: $first was recieved" -ForegroundColor Green
Write-host "Last: $Last was recieved" -ForegroundColor magenta
Write-host "Dimension: $Dimension was recieved" -ForegroundColor Cyan
}

$obj | PipePropertyTest
$obj2 | PipePropertyTest #ERROR TOWN POPULATION: YOU

function PipePropertyTestAlias
{
param([parameter(ValueFromPipelineByPropertyName)]
[string][alias("FirstName")]$First,
[parameter(ValueFromPipelineByPropertyName)]
[string][alias("LastName")]$Last,
[parameter(ValueFromPipelineByPropertyName)][string]
[alias("Location","testAlias")]$Dimension)
Write-host "Fist: $first was recieved" -ForegroundColor Green
Write-host "Last: $Last was recieved" -ForegroundColor magenta
Write-host "Dimension: $Dimension was recieved" -ForegroundColor Cyan
}

$obj | PipePropertyTestAlias
$obj2 | PipePropertyTestAlias
PipePropertyTestAlias -First "Jerry" -Last "Smith" -testAlias "see how I can use them inline too?"

This got a bit longer than planned so thatā€™s all for now. I’ll do another post talking about some other attributes you have access to. Hopefully this helps you get going on your PowerShell journey and lets you level up your toolset to be as professional and feature rich as possible. If you’re looking to add any specific functionality to a tool that I didn’t cover, just let me know and I can add it for a future post!

For the main series post, check backĀ here.

If you find this helpful don’t forget to rate, comment and shareĀ šŸ™‚

Author

Kory Thacher
Premier Field Engineer

I've been a PFE since 2012, working with various technologies. I live in the modern applications domain, doing work in UWP apps, .NET, Unity, DevOps, and of course PowerShell. I've been teaching PowerShell related workshops very frequently for years, and I really enjoy getting the opportunity to explain a topic or learn something new from my colleagues. I enjoy scripting because of how fast and interactive it can be, and I love getting interesting problems to work on with customers. ...

More about author

0 comments

Discussion are closed.