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, _
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, _
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.
No comments:
Post a Comment