Create a Truly Advanced PowerShell Function

Create a Truly Advanced PowerShell Function

  • Comments 4
  • Likes

 

Summary: Learn how to create a true advanced function that interacts with the Windows PowerShell pipeline. 

Microsoft Scripting Guy Ed Wilson here. Sean Kearney, Microsoft Windows PowerShell MVP, has been burning up the keyboard lately with ideas for guest blog posts. I hate to disappoint him by not sharing his hard work with you. (I am sincerely appreciative of Sean’s hard work and willingness to share). Today Sean has written about using advanced functions and the pipeline.

Take it away Sean…

 

I played this weekend because a good friend mentioned that my output from a blog post was only returning a hash table (and a messy one at that). Thanks, Klaus. I sat down this weekend and learned something.

You see, initially when I learned about advanced functions (as an IT pro just hacking about), I found that all I had to do to make it work was tack Function NameofFunction() at the top of a script, and put a pair of curly brackets { } near the top and bottom, and it worked.

Well…really…it sort of worked because I wasn’t adding in some key pieces.

So this weekend I wanted to really learn it right. Here’s a simple script. It’s stupid, it’s pointless, and it’s an example…

param [String]$FullName

$FooT=$FullName.split("/")
$FooU=$FullName.toUPPER()
$FooD=$FullName.substring(3,2)

Return $FooT,$FooU,$FooD

All this script does is take the string parameter $FullName, do some crazy stuff to the value, and spit it out as three objects. It would have been run like this:

$HereisSomethingToPlayWith=”C:\My\Stuff\Should\Not\Be\On\The\Floor\BigMess.txt”

./MYSILLYSCRIPT.PS1 $HereisSomethingToPlayWith

What I wanted to do first was to get this to return a single object. That is done by using New-Object and defining a custom object as shown here.

$StuffBack=NEW-OBJECT PSOBJECT -property @{This='';That='';SomethingElse=''}

You can edit the individual parts of the object like this...

$StuffBack.This=$FooT
$StuffBack.That=$FooU
$StuffBack.Somethingelse=$FooD

…which will allow us to do this:

param [String]$FullName

$StuffBack=NEW-OBJECT PSOBJECT -property @{This='';That='';SomethingElse=''}

$FooT=$FullName.split("/")
$FooU=$FullName.toUPPER()
$FooD=$FullName.substring(3,2)

$StuffBack.This=$FooT
$StuffBack.That=$FooU
$StuffBack.Somethingelse=$FooD

Return $StuffBack

This will give us one object, which is a lot cleaner on the output and “pipeline friendlier.”

Now the fun stuff. If we want to take this to the next level and make it into a proper advanced function as opposed to only a .ps1 file, of course we put the function part at the top; but also, as part of a parameter, we make some changes. First, place the function name at the top of the script and add a curly bracket like so { at the top, and add one like so } as the last line in the script. I also would like to have this function available outside of the script as shown here:

Function Global:GET-foo()
{

We need to tack on a [CmdLetBinding], which states that this function should act just like a cmdlet. We also need to specify if the variable can accept input from the pipeline. This is done by changing the original parameter. And we need to specify that the content is now accepting an array of information (whether it’s one line or 1,000 lines from the pipeline, it’s still an array). Now here’s something I’m going to do for fun—I’m going to tell this particular advanced function that it is only to accept information from the pipeline if there is a property called FullName.

By the way, all of this information can be found in the Help in the Windows Powershell ISE by searching for “about_Functions_Advanced.” Or you can run GET-HELP about_Functions_Advanced.

So we’re going to change this...

param [String]$FullName

…to this:

[CmdletBinding()]
Param(
[parameter](Mandatory=$True,
$ValueFromPipeline=$True)
$ParameterSetName=’FullName’]
[string[]]$FullName
)

Now here’s the fun bit. You have to create a block in your script called Process. What this block says is “Any lines coming down the pipeline, I process here, and here is the code we’re going to run against those.” In the case of our original script, we want to have that as the pipeline process, so place the word Process just before your main block of code.

But there’s another piece to watch out for. When you’re working with any of your previous values, you need to remember that you’re now working with an array. So although the Process block is only reading one line at a time, it is now one line in an array. You’ll have to edit the code to reflect that.

So our original block changes from this...

$FooT=$FullName.split("/")
$FooU=$FullName.toUPPER()
$FooD=$FullName.substring(3,2)

…to this now:

$FooT=$FullName[0].split("/")
$FooU=$FullName[0].toUPPER()
$FooD=$FullName[0].substring(3,2)

The end result, if we put all the pieces together, is shown here:

Function global:GET-foo()
{

[CmdletBinding()]
Param(
[parameter](Mandatory=$True,
$ValueFromPipeline=$True)
$ParameterSetName=’FullName’]
[string[]]$FullName
)

Process
{

#Pointless Process

$StuffBack=NEW-OBJECT PSOBJECT -property @{This='';That='';SomethingElse=''}

$FooT=$FullName[0].split("/")
$FooU=$FullName[0].toUPPER()
$FooD=$FullName[0].substring(3,2)

$StuffBack.This=$FooT
$StuffBack.That=$FooU
$StuffBack.Somethingelse=$FooD

Return $StuffBack

}

}

Now when we run our script, we’ll get a new cmdlet called Get-Foo. Here’s the neat bit. Type Get-Help, and you’ll actually see some rudimentary Help about it. You can press Tab to autocomplete the command name (using Tab to autocomplete a command also works on parameters).

Remember that we said to accept only properties from the pipeline called FullName? I did this intentionally for this example. If you run this…

GET-CHILDITEM | GET-FOO

…it will pull down the FullName property from Get-ChildItem for each individual object, and then mess with it.

Yes, the actual function is completely pointless and trivial. But hopefully, you can see the basics about creating an advanced function and manipulating data in the pipeline.

Remember the Power of Shell is in you!

Sean
The Energized Tech

 

Thank you, Sean, for a great article about creating an advanced function in Windows PowerShell.

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

 

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • I think this is what you are trying to do.  Notice the use of the ParameterName and the ParameterSetName.  They are different.

    NOt ehow we can unwind an array when passed. Your method works but can lead ot odd behaiors if teh code is cahnged in certain ways.

    I also find it cleaner to generate teh output object explicitly at the end.  The PoewerShell guys have given 'return' a bad name for some reason so I have almost nevr used it.

    I also prefer the stacking-style for the code layout as it seems easier to see mistakes. Actually this used to be the default style in Visual Studio with 'C' languages until VS 2005 when it changed.

    Here is my take on how it should look along with some bug fixes and three examples.

    Function global:Get-Foo{

        [CmdletBinding(DefaultParameterSetName='Namer')]

        Param(

            [parameter(

                 ParameterName='FullName',

                 Position=1,

                 Mandatory=$True,

                 ValueFromPipeline=$True,

                 ParameterSetName='Namer'

            )][string[]]$FullName

        )

        Process{

             #Pointless Process

             $FullName |

             ForEach-Object{

                  $FooT=$_.split("/")

                  $FooU=$_.toUPPER()

                  $FooD=$_.substring(3,2)

                  New-Object PSObject -property @{This=$FooT;That=$FooU;SomethingElse=$FooD}

             }

        }

    }

    $list='C:\My\Stuff\Should\Not\Be\BigMess.txt','C:\My\Stuff\Be\On\The\Floor\BigMess.txt'

    $list | Get-Foo

    Get-Foo $list

    Get-Foo -fullname $list

    download as text --->

    www.designedsystemsonline.com/.../get-foo.txt

  • Cliff

  • Hello Sean,

    intersting stuiff ... these advanced functions!

    This is a good intro into "how to make an adv. function out of nearly nothing" :-)

    But: There are several mistakes between the lines starting with "Param(" and ending five lines later with ")"

    A corrected version should look like

    [CmdletBinding()]

    Param(

    [parameter(Mandatory=$True,

    ValueFromPipeline=$True,

    ParameterSetName='FullName')]

    [string[]]$FullName

    )

    "the energized fox junped too quick" :-)

    Klaus

  • @Klaus

    Ah you caught me.   My script was working but my typing when I transposed it to the blogpost apparently dropped a few bits!

    Guess I should have had that extra pot of coffee :)

    Thanks for keeping me on my toes.  It's only through collaboration we can all improve!  And I was serious Klaus, this article happened because you pointed something out to me :=)

    Keep that shell a Rockin'!