Friday, December 30, 2011

Powershell - Add-Member -MemberType ScriptMethod

I've been using a lot of new Powershell features (v2) and doing a lot of research accordingly.  While watching Twitter recently I saw this one from @jonwalz fly past and it got me thinking about Add-Member again:
Get-Service | Add-Member ScriptMethod ToString { $this.Name } -Force -PassThru | Group-Object Status ?
To get an understanding of how ScriptMethod is being used here I started reversing the one-liner.  I know, from the help, that Get-Member's MemberType parameter allows you to specify ScriptMethod, but, hadn't really seen any practical working examples.  The help notes this:
ScriptMethod: A method whose value is the output of a script.
I Googled around quickly and found one of Dmitry Sotnikov's post:
http://dmitrysotnikov.wordpress.com/2008/08/27/select-object-vs-add-member/
which opened up entirely different directions to research/test, but, he gave a practical example of how Add-Member works, but, not ScriptMethod.  Next, I found this, which not only explains it, but, shows how it works and touches on -PassThru (one of my little personal unknowns):
http://get-powershell.com/post/2009/02/18/A-Method-to-the-Add-Member-Madness.aspx
From reading Andy's post I learned the following about ScriptMethod:
  • when using ScriptMethod the method automatically references $this. He touches on what $this is briefly: "$this refers to the current object. It is similar to the "this" keyword in C#."
  • you can explicitly declare a new variable of type [ScriptBlock], but, if it's left out, it is automatically assumed by the system that you are passing something of this type. 
In the example posted earlier, a function is written, called ToString, which simply grabs the .Name property of the current object in the pipeline.  To poke at the pipeline, after I chop it into smaller bits, I like to use .GetType() (or, foreach and .GetType() if I am dealing with a collection) and Get-Member.  Knowing what you're dealing with and it's members is fundamental to getting things down.  In this case, I went with the first cmdlet and got this:
(Get-Service).GetType()
returns:
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array
So,
Get-Service | Get-Member
would return a list of System.Array members. Not what I was hoping for.  Let's try this again.  This is one of those cases, where we are dealing with collections, so, I wanted to look at one object in the collection (the old one versus many dilemma).  Using this syntax
(Get-Service | Select-Object -First 1).GetType()
I get a very different result from GetType()
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    ServiceController                        System.ComponentModel.Component
and | Get-Member,
(Get-Service | Select-Object -First 1)  | Get-Member


TypeName: System.ServiceProcess.ServiceController

Name MemberType Definition
---- ---------- ----------
Name AliasProperty Name = ServiceName
RequiredServices AliasProperty RequiredServices = ServicesDependedOn
Disposed Event System.EventHandler Disposed(System.Object, System.EventArgs)
Close Method System.Void Close()
Continue Method System.Void Continue()
CreateObjRef Method System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
Dispose Method System.Void Dispose()
Equals Method bool Equals(System.Object obj)
ExecuteCommand Method System.Void ExecuteCommand(int command)
GetHashCode Method int GetHashCode()
GetLifetimeService Method System.Object GetLifetimeService()
GetType Method type GetType()
InitializeLifetimeService Method System.Object InitializeLifetimeService()
Pause Method System.Void Pause()
Refresh Method System.Void Refresh()
Start Method System.Void Start(), System.Void Start(string[] args)
Stop Method System.Void Stop()
ToString Method string ToString()
WaitForStatus Method System.Void WaitForStatus(System.ServiceProcess.ServiceControllerStatus desi...
CanPauseAndContinue Property System.Boolean CanPauseAndContinue {get;}
CanShutdown Property System.Boolean CanShutdown {get;}
CanStop Property System.Boolean CanStop {get;}
Container Property System.ComponentModel.IContainer Container {get;}
DependentServices Property System.ServiceProcess.ServiceController[] DependentServices {get;}
DisplayName Property System.String DisplayName {get;set;}
MachineName Property System.String MachineName {get;set;}
ServiceHandle Property System.Runtime.InteropServices.SafeHandle ServiceHandle {get;}
ServiceName Property System.String ServiceName {get;set;}
ServicesDependedOn Property System.ServiceProcess.ServiceController[] ServicesDependedOn {get;}
ServiceType Property System.ServiceProcess.ServiceType ServiceType {get;}
Site Property System.ComponentModel.ISite Site {get;set;}
Status Property System.ServiceProcess.ServiceControllerStatus Status {get;}
Alright, now we're cooking with grease.  I know exactly what I get from cmdlet 1.  Now, let's add a member and get the type:
(Get-Service | Add-Member -MemberType ScriptMethod ToString { $this.Name } -Force -PassThru | Select-Object -first 1).GetType()
returns this, which as to be expected, is the same as the definition above,
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    ServiceController                        System.ComponentModel.Component
There's actually a lot that goes into this line.  The Add-Member cmdlet is adding a new ScriptMethod to the object called ToString.  The ScriptMethod, which, remember, is of type [ScriptBlock], has the definition.
ToString { $this.Name }
So, when we select the object after the member has been added, using the Select-Object cmdlet, it adds a new ScriptMethod member.  We can verify this by looking at the pipelined command and using Get-Member.  To do this, I use this command,
(Get-Service | Add-Member -MemberType ScriptMethod ToString { $this.Name } -Force -PassThru
and it returns,
   TypeName: System.ServiceProcess.ServiceController
Name                      MemberType    Definition
----                      ----------    ----------
Name                      AliasProperty Name = ServiceName
RequiredServices          AliasProperty RequiredServices = ServicesDependedOn
Disposed                  Event         System.EventHandler Disposed(System.Object, System.EventArgs)
Close                     Method        System.Void Close()
Continue                  Method        System.Void Continue()
CreateObjRef              Method        System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType)
Dispose                   Method        System.Void Dispose()
Equals                    Method        bool Equals(System.Object obj)
ExecuteCommand            Method        System.Void ExecuteCommand(int command)
GetHashCode               Method        int GetHashCode()
GetLifetimeService        Method        System.Object GetLifetimeService()
GetType                   Method        type GetType()
InitializeLifetimeService Method        System.Object InitializeLifetimeService()
Pause                     Method        System.Void Pause()
Refresh                   Method        System.Void Refresh()
Start                     Method        System.Void Start(), System.Void Start(string[] args)
Stop                      Method        System.Void Stop()
WaitForStatus             Method        System.Void WaitForStatus(System.ServiceProcess.ServiceControllerStatus desi...
CanPauseAndContinue       Property      System.Boolean CanPauseAndContinue {get;}
CanShutdown               Property      System.Boolean CanShutdown {get;}
CanStop                   Property      System.Boolean CanStop {get;}
Container                 Property      System.ComponentModel.IContainer Container {get;}
DependentServices         Property      System.ServiceProcess.ServiceController[] DependentServices {get;}
DisplayName               Property      System.String DisplayName {get;set;}
MachineName               Property      System.String MachineName {get;set;}
ServiceHandle             Property      System.Runtime.InteropServices.SafeHandle ServiceHandle {get;}
ServiceName               Property      System.String ServiceName {get;set;}
ServicesDependedOn        Property      System.ServiceProcess.ServiceController[] ServicesDependedOn {get;}
ServiceType               Property      System.ServiceProcess.ServiceType ServiceType {get;}
Site                      Property      System.ComponentModel.ISite Site {get;set;}
Status                    Property      System.ServiceProcess.ServiceControllerStatus Status {get;}
ToString                  ScriptMethod  System.Object ToString();
Notice the last line, ToString. This is what we accomplished with the Add-Member cmdlet.  Yet, I didn't touch on the two addition parameters: -PassThru and -Force.  As outlined in Get-Help,
Get-Help Add-Member -Parameter Passthru

-PassThru []
Passes the newly extended object to the pipeline. By default, this cmdlet does not generate any output.

Required? false
Position? named
Default value
Accept pipeline input? false
Accept wildcard characters? false
As it was explained to me, this is the same thing C# does when returning the object by specifying the datatype instead of void. In practice, this adds the new member, but, returns no result to be handle except for the extended object. -PassThru, from my understanding, is a part of the Extended Type System.  If you do not add -PassThru to the command outlined in this example, it will fail to return anything:
(Get-Service | Add-Member -MemberType ScriptMethod ToString { $this.Name } -Force | Select-Object -first 1) | Get-Member
returns nothing. We can verify this with .GetType().
(Get-Service | Add-Member -MemberType ScriptMethod ToString { $this.Name } -Force | Select-Object -first 1).GetType()
throws this error:
You cannot call a method on a null-valued expression.
At line:1 char:116
+ (Get-Service | Add-Member -MemberType ScriptMethod ToString { $this.Name } -Force | Select-Object -first 1).GetType < <<< () + CategoryInfo : InvalidOperation: (GetType:String) [], RuntimeException
Aaron Nelson also pointed me to this post to get a little further clarification on its usage:
http://powershell.com/cs/blogs/tips/archive/2009/08/26/adding-custom-properties.aspx
The -Force parameter merely causes the change to occur regardless of any objections by Powershell.
Get-Help Add-Member -Parameter Force

-Force []
Adds a new member even if one with the same name already exists. Does not work for core members of a type.

Required? false
Position? named
Default value
Accept pipeline input? false
Accept wildcard characters? false
The last portion of the script above, Group, is well-documented, so, I won't worry about it too much. 

0 comments:

Post a Comment