Monday, January 15, 2007

PowerShell CmdLet Parameters 101

For all of you who are attempting to write a CmdLet ;

"We salute you!"

Ok! Ok1 I'll turn my Ipod off.

I decided to build up a CmdLet from "First Principals" and where better to start than with an exploration of the "input"  into a CmdLet.

CmdLet input is accomplished in a myriad of ways.  It can come from the values/objects listed after the CmdLet name, from the pipeline, from ScripBlocks or from Parameters and ParameterSets.  I will try to break these down by starting with, from the perspective of the CmdLet code,  the simplest case first then add each next option.

I will start with building a trivial CmdLet; "Test-CmdLet",  Here is the code.

Imports System.Management.Automation

<Cmdlet(VerbsDiagnostic.Test, "Cmdlet", SupportsShouldProcess:=True)> _
Public Class Test_Cmdlet
    Inherits Cmdlet

    Private _Name As String
    Public Property Name() As String
        Get
            Return _Name
        End Get
        Set(ByVal value As String)
            _Name = value
        End Set
    End Property

    Protected Overrides Sub ProcessRecord()
        Try
            WriteObject(Me)
        Catch ex As Exception

        End Try
    End Sub

End Class

You can see that the above code is trivial.  It is a basic "do-nothing"  Class template with one Property, "Name",  and returns a reference to itself.  For now I am using this returned "self" reference as a simple means to capture the CmdLet object and inspect it.

What happens when we run this in PoSH?

PS>test-cmdlet

Name             Stopping             CommandRuntime
------------------------------------------ -------- --------------
                           False                      Test-Cmdlet

It tells us that it has a "Name"  property and that the property is empty.   Let's try to set the name property.

PS>test-cmdlet SomeName
Test-Cmdlet : A parameter cannot be found that matches parameter name 'SomeName'.
At line:1 char:12
+ test-cmdlet <<<< SomeName

Can we set the Property at all?

PS>$t = test-cmdlet
PS>$t.Name = "SomeName"
PS>$t

Name                 Stopping        CommandRuntime
---- -------- -------------------------------------------
SomeName     True                 Test-Cmdlet

Yes, we can assign a value to the "Name" property so why can't we do it from the argument list?

Let's explore the "Parameter" attribute. These little "decorations" that don't seem to show up in the code are actually little hints to the compiler that tell it to generate specific information in the building of a class and in the generation of the assembly.  PowerShell has leveraged these to make it easier for programmers to "hook-up" their code to the PowerShell system.

The simplest - (First Principals) - decoration we can use is to add the "Parameter" attribute decoration:

<Parameter()> _

Public Property Name() As String

Notice that it ends in an underscore.  The underscore in VB tells us that it is really all on one line.  For convenience, readability and ease of use the "Parameter" attribute is generally built over many lines; one line for each optional argument.

After a rebuild of the code our test looks like this:

PS>test-cmdlet SomeName
Test-Cmdlet : A parameter cannot be found that matches parameter name 'SomeName'.
At line:1 char:12
+ test-cmdlet <<<< SomeName

Still can't find the input....

How about if we give the CmdLet a hint.

PS>test-cmdlet -Name SomeName

Name                            Stopping                      CommandRuntime
---- -------------------------------------------------------------------
SomeName                False                             Test-Cmdlet

Now that's better.  We can assign the property by specifying it's name on the command line as a parameter using the "dash" to tell PoSH that we are asking for it to find a parameter with the name "Name"

The next step is fairly obvious.  How to get the CmdLet to pick up the name from the arguments passed "positionally" to the CmdLet.

<Parameter(Position:=0)> _

Public Property Name() As String

Now when we give PoSH the CmdLet like this "test-cmdlet somename" the value is picked up and automatically assigned to the parameter because of it's position on the command line.

PS>test-cmdlet SomeName

Name               Stopping          CommandRuntime
--------------------------------------------------------
SomeName    False                 Test-Cmdlet

What happens if we send an object through the pipeline to our CmdLet?  Wouldn't it be nice if the CmdLet could get it's parameters from the object we pass?  Let's see what happens if we pass a process object through the pipeline?

PS>$p[0]|test-cmdlet
Test-Cmdlet : The input object cannot be bound to any parameters for the command either because the command does not ta
ke pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
At line:1 char:17
+ $p[0]|test-cmdlet <<<<

PoSH can't find any valid parameters. 

But the process objects have a "Name" property. Why can't PoSH find it and "bind" it to our CmdLet?

Maybe we need to explicitly tell PoSH that a parameter is available for binding.

<Parameter( _

           Position:=0, _

        ValueFromPipeLine:=true )> _

Let's try again.

PS>$p[0]|test-cmdlet

Name                                                               Stopping           CommandRuntime
---- -------- --------------
System.Diagnostics.Process (alg)       False         Test-Cmdlet

The Name, in orange, looks a little strange.  It's telling us that "Name" has been set to the string value of the process object class instance; hence the "alg" in parens.  Well that's a little better.  We are bainding to the input object but the bind seems to be to the object itself and no teh propery we want.

We wanted the actual name of the process - "alg".  How can we get this.  Maybe we need more hints.  We told PoSH to take the "Name" parameter from the pipeline but didn't say how to bind it.  Let's tell PoSH to bind it by property name.

<Parameter( _

           Position:=0, _

          ValueFromPipelineByPropertyName:=True,  _

        ValueFromPipeLine:=true )> _

PS>$p=get-process
PS>$p | test-cmdlet

Name                   Stopping                         CommandRuntime
----                       --------                        --------------
alg                        False                                Test-Cmdlet
cmd                     False                                Test-Cmdlet
csrss                   False                                Test-Cmdlet
ctfmon               False                                Test-Cmdlet
dexplore           False                                Test-Cmdlet

Now the pipeline can find the property name and bind to it so that our CmdLet can get it's input property values from the incoming object.

 Now I am going to go back and remove the reference to "ValueFromPipeLine" and test.  This parameter is not really necessary for this type and level of binding so lets leave it out and use only:

<Parameter( _

           Position:=0, _

          ValueFromPipelineByPropertyName:=True)> _

This is the minimal set of attribute values to use to get both positional binding and binding by property name.

This may seem like a lot of detail for such trivial things.  After all, doesn't the SDK documentation tell us all of this.  Yes...but?  In order to debug parameter binding issues quickly and in order to fully understand what PowerShell brings to the table I feel it is worth this in depth discussion.   It took me a little while to track this information down in the SDK.  I found that the SDK had many deficiencies in it's approach mostly due to the need to document and and not to explain.

Many of the examples in the SDK are workable but don't address many of the questions a developer might have.  I am sure that many books will be written on the PowerShell SDK.  I recommend reading them if you plan serious development in PowerShell.  For those who need to quickly build a CmdLet I hope that this blog entry will be useful and save you some time.

In the next chapter I will attack some of the other "Parameter" attribute values and how they add to the ease of CmdLet development and provide dramatically enhanced capabilities to the baseline code.

 

Technorati tags: ,

 

del.icio.us tags: ,

No comments:

Post a Comment