Chapter 20. Your Own Cmdlets and Extensions

Since PowerShell is layered on the .NET framework, you already know from Chapter 6 how you can use .NET code in PowerShell to make up for missing functions. In this chapter, we'll take up this idea once again. You'll learn about the options PowerShell has for creating command extensions on the basis of the .NET framework. You should be able to even create your own cmdlets at the end of this chapter.

Topics Covered:

Compiling Your Own .NET Expansions

Many functionalities of the .NET framework are available right in PowerShell. For example, the following two lines suffices to set up a dialog window:

[System.Reflection.Assembly]::`
LoadWithPartialName("Microsoft.VisualBasic")
[Microsoft.VisualBasic.Interaction]::`
MsgBox("Do you agree?", "YesNoCancel,Question", "Question")

In Chapter 6, you learned in detail about how this works and what an "assembly" is. To briefly explain what happened here, PowerShell used LoadWithPartialName() to load a system library and was then able to use the classes from it to call a static method like MsgBox().

That's extremely practical when there is already a system library that offers the method you're looking for, but for some functionality even the .NET framework doesn't have any right commands. For example, you have to rely on your own resources if you want to move text to the clipboard. The only way to get it done is to access the low-level API functions outside the .NET framework.

Extension for the Clipboard

As soon as you need more than just a few lines of code or access to API functions to implement the kinds of extensions you want, it makes sense to write the extension directly in .NET program code. The following example shows how a method called CopyToClipboard() might look in VB.NET. The VB.NET code is directly assigned in the form of a Here string to the $code variable:

$code = @'
Imports Microsoft.VisualBasic
Imports System
Namespace ClipboardAddon
Public Class Utility
Private Declare Function OpenClipboard Lib "user32" _
(ByVal hwnd As Integer) As Integer
Private Declare Function EmptyClipboard Lib "user32" _
() As Integer
Private Declare Function CloseClipboard Lib "user32" _
() As Integer
Private Declare Function SetClipboardData Lib "user32" _
(ByVal wFormat As Integer, ByVal hMem As Integer) As Integer
Private Declare Function GlobalAlloc Lib "kernel32" _
(ByVal wFlags As Integer, ByVal dwBytes As Integer) As Integer
Private Declare Function GlobalLock Lib "kernel32" _
(ByVal hMem As Integer) As Integer
Private Declare Function GlobalUnlock Lib "kernel32" _
(ByVal hMem As Integer) As Integer
Private Declare Function lstrcpy Lib "kernel32" (ByVal _
lpString1 As Integer, ByVal lpString2 As String) As Integer

Public Sub CopyToClipboard(ByVal text As String)
Dim result As Boolean = False
Dim mem As Integer = GlobalAlloc(&H42, text.Length + 1)
Dim lockedmem As Integer = GlobalLock(mem)

lstrcpy(lockedmem, text)
If GlobalUnlock(mem) = 0 Then
If OpenClipboard(0) Then
EmptyClipboard()
result = SetClipboardData(1, mem)
CloseClipboard()
End If
End If
End Sub
End Class
End Namespace
'@

You have to first compile the code before PowerShell can execute it. Compilation is a translation of your source code into machine-readable intermediate language (IL). There are two options here.

In-Memory Compiling

In a very simple case, you can task PowerShell to use CompileAssemblyFromSource() to translate your source code directly in a memory. The result is a new .NET assembly. As soon as the assembly is compiled, PowerShell can use the methods in it as well as CopyToClipboard() to move text to the clipboard:

$provider = New-Object Microsoft.VisualBasic.VBCodeProvider
$params = New-Object System.CodeDom.Compiler.CompilerParameters
$params.GenerateInMemory = $True
$refs = "System.dll","Microsoft.VisualBasic.dll"
$params.ReferencedAssemblies.AddRange($refs)
$results = $provider.CompileAssemblyFromSource($params, $code)
$object = New-Object clipboardaddon.Utility
$object.CopyToClipboard("Hi Everyone!")

You might be asking yourself why you have to use New-Object first to create a new object in order to call your CopyToClipboard() method? That wasn't necessary in the first example of the MsgBox() method.

CopyToClipboard() is created in your source code as a dynamic method, which requires you to first create an instance of the class, and that's exactly what New-Object does. Then the instance can call the method.

Alternatively, methods can also be static. For example, MsgBox() in the first example is a static method. To call static methods, you need neither New-Object nor any instances. Static methods are called directly through the class in which they are defined.

If you would rather use CopyToClipboard()as a static method, all you need to do is to make a slight change to your source code. Replace this line:

Public Sub CopyToClipboard(ByVal text As String)

Type this line instead:

Public Shared Sub CopyToClipboard(ByVal text As String)

Once you have compiled your source code, then you can immediately call the method like this:

$provider = New-Object Microsoft.VisualBasic.VBCodeProvider
$params = New-Object System.CodeDom.Compiler.CompilerParameters
$params.GenerateInMemory = $True
$refs = "System.dll","Microsoft.VisualBasic.dll"
$params.ReferencedAssemblies.AddRange($refs)
$results = $provider.CompileAssemblyFromSource($params, $code)
[clipboardaddon.Utility]::CopyToClipboard("Hi Everyone!")

DLL Compilation

You'll lose your compilation in the memory as soon as you end PowerShell, which means you would have to do everything all over again every time you need the CopyToClipboard() method. An often better approach is to compile your source code in a Dynamic Link Library (DLL), whose file can then be loaded whenever you need it or passed on to friends and colleagues.

To make a DLL file from your source code, call the vbc.exe VB.NET compiler directly:

$code | Out-File sourcecode.vb
$path = Resolve-Path sourcecode.vb
$compiler = "$env:windir/Microsoft.NET/Framework/v2.0.50727/vbc"
&$compiler /target:library $path
dir sourcecode.dll

The result is the sourcecode.dll file. If you want to put it to work, all you have left to do now is to use LoadFrom() to load it in PowerShell:

$path = Resolve-Path sourcecode.dll
[System.Reflection.Assembly]::LoadFrom($path)
$object = New-Object clipboardaddon.Utility
$object.CopyToClipboard("Hi Everyone!")

If you'd rather compile c# code, simply use the csc.exe c# compiler instead of vbc.exe. Use LoadFrom() if you want to load assemblies from any DLLs again and LoadWithPartialName() if you want to load system assemblies that are registered in the central .NET Global Assembly Cache (GAC).

Building Your Own Cmdlets

Command extensions based on DLLs that you compile yourself are an interesting alternative, but somewhat unwieldy. You would have to already know exactly where to find the DLL, use LoadFrom() to load it, and still have to know which method in the assembly is the right one. A further disadvantage is that methods from external DLLs don't support the PowerShell pipeline.

Your command extension will work much more conveniently if you turn it into a cmdlet. Your own new cmdlet will behave just like cmdlets that already exist. You can put it to work inside the pipeline, and it won't require any unusual method invocations.

How Cmdlets Are Structured

Every cmdlet represents a single command. These are wrapped as a package in the form of a snap-in so that PowerShell can use your cmdlets. The following example makes use of your clipboard function above in the Out-Clipboard cmdlet and wraps this in the ClipboardSnapin snap-in. A little later, it will be explained just how the following source code works. First, take a look at what steps are necessary to make this source code into a functioning cmdlet:

$code = @'
Imports System
Imports System.Configuration.Install
Imports System.Collections.Generic
Imports System.Text
Imports System.ComponentModel
Imports System.Management.Automation

Namespace MSPressBuch.PowerShell.Cmdlets
<RunInstaller(True)> Public Class ClipboardSnapin
Inherits PSSnapIn

Public Sub New()
MyBase.New()
End Sub

Public Overrides ReadOnly Property Name() As String
Get
Return "Clipboard-Tool"
End Get
End Property

Public Overrides ReadOnly Property Vendor() As String
Get
Return "Dr. Tobias Weltner"
End Get
End Property

Public Overrides ReadOnly Property VendorResource() As String
Get
Return String.Format("{0},{1}", Name, Vendor)
End Get
End Property

Public Overrides ReadOnly Property Description() As String
Get
Return "Copy text to clipboard"
End Get
End Property

Public Overrides ReadOnly Property _
DescriptionResource() As String
Get
Return String.Format("{0},{1}", Name, Description)
End Get
End Property
End Class

<Cmdlet(VerbsData.Out, "Clipboard")> Public Class ClipboardHelper
Inherits Cmdlet

Private data As String = ""
Private _Text() As String
Private linefeed as string = ""

<Parameter(Mandatory:=False, _
Position:=0, _
ValueFromPipeline:=True, _
HelpMessage:="Text to copy to Clipboard"), _
ValidateNotNullOrEmpty()> _
Public Property Text() As String()
Get
Return _Text
End Get
Set(ByVal value As String())
_Text = value
End Set
End Property

Protected Overrides Sub BeginProcessing()
WriteDebug("Enter BeginProcessing")
data = ""
MyBase.BeginProcessing()
End Sub

Protected Overrides Sub EndProcessing()
WriteDebug("Enter EndProcessing")
Utility.CopyToClipboard(data)
MyBase.EndProcessing()
End Sub

Protected Overrides Sub ProcessRecord()
WriteDebug("Processing Record")
Try
For Each Line As String In _Text
data += Line
data += linefeed
linefeed = (chr(13) + chr(10))
Next
Catch ex as Exception
WriteDebug("Failure: " & ex.Message)
End Try
WriteDebug("Done Processing Record")
End Sub
End Class

Public Class Utility
Private Declare Function OpenClipboard Lib "user32" _
(ByVal hwnd As Integer) As Integer
Private Declare Function EmptyClipboard Lib "user32" _
() As Integer
Private Declare Function CloseClipboard Lib "user32" _
() As Integer
Private Declare Function SetClipboardData Lib "user32" _
(ByVal wFormat As Integer, ByVal hMem As Integer) _
As Integer
Private Declare Function GlobalAlloc Lib "kernel32" _
(ByVal wFlags As Integer, ByVal dwBytes As Integer) _
As Integer
Private Declare Function GlobalLock Lib "kernel32" _
(ByVal hMem As Integer) As Integer
Private Declare Function GlobalUnlock Lib "kernel32" _
(ByVal hMem As Integer) As Integer
Private Declare Function lstrcpy Lib "kernel32" _
(ByVal lpString1 As Integer, ByVal lpString2 As _
String) As Integer

Public Shared Sub CopyToClipboard(ByVal text As String)
Dim result As Boolean = False
Dim mem As Integer = GlobalAlloc(&H42, text.Length + 1)
Dim lockedmem As Integer = GlobalLock(mem)
lstrcpy(lockedmem, text)
If GlobalUnlock(mem) = 0 Then
If OpenClipboard(0) Then
EmptyClipboard()
result = SetClipboardData(1, mem)
CloseClipboard()
End If
End If
End Sub
End Class
End Namespace
'@

Step 1: Compiling the Snap-In

As in the first examples, your source code must first be compiled. Again, use the vbc.exe VB.NET compiler. However, because your cmdlet uses classes like Cmdlet and PSSnapIn, which PowerShell provides, the compiler has to be given a reference to the PowerShell libraries. Use the /reference switch to give this reference. The simplest way to find the location of the PowerShell libraries is to query the Assembly.Location property of the PSObject type.

$code | Out-File cmdlet.vb
$path = Resolve-Path cmdlet.vb
$compiler = "$env:windir/Microsoft.NET/Framework/v2.0.50727/vbc"
$ref = [PsObject].Assembly.Location
&$compiler /target:library /reference:$ref $path
dir cmdlet.dll
Directory: Microsoft.PowerShell.Core\FileSystem::
C:\Users\Tobias Weltner

Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 10.24.2007 12:13 8192 cmdlet.dll

If you haven't made any typing errors in $code of your source code, you'll get the file cmdlet.dll: your new snap-in, which includes your cmdlet.

Step 2: Registering Snap-Ins

Before you can use a snap-in, it has to be registered. The installutil.exe utility program of the .NET framework handles registration. Using it is roughly like using regsvr32.exe to register COM components in the old COM world. It makes some registry entries so that PowerShell can find your snap-in later.

Registering PowerShell snap-ins requires administrator privileges.

The following lines implement registration:

$path = Resolve-Path cmdlet.dll
$register = "$env:windir/Microsoft.NET/Framework/v2.0.50727/installutil"
&$register $path
Microsoft (R) .NET Framework Installation utility Version
2.0.50727.312
Copyright (c) Microsoft Corporation. All rights reserved.

Running a transacted installation.

Beginning the Install phase of the installation.
See the contents of the log file for the C:\Users\
Tobias Weltner\cmdlet.dll assembly's progress.
The file is located at C:\Users\Tobias Weltner\
cmdlet.InstallLog.
Installing assembly 'C:\Users\Tobias Weltner\cmdlet.dll'.
Affected parameters are:
logtoconsole =
assemblypath = C:\Users\Tobias Weltner\cmdlet.dll
logfile = C:\Users\Tobias Weltner\cmdlet.InstallLog

The Install phase completed successfully, and the
Commit phase is beginning.
See the contents of the log file for the C:\Users\
Tobias Weltner\cmdlet.dll assembly's progress.
The file is located at C:\Users\Tobias Weltner\
cmdlet.InstallLog.
Installing assembly 'C:\Users\Tobias Weltner\cmdlet.dll'.

Affected parameters:
logtoconsole =
assemblypath = C:\Users\Tobias Weltner\cmdlet.dll
logfile = C:\Users\Tobias Weltner\cmdlet.InstallLog

The Commit phase completed successfully.

The transacted install has completed.

Step 3: Loading Snap-Ins

You use the Get-PSSnapin cmdlet to manage all registered snap-ins. This cmdlet allows you to find out which snap-ins that you are currently using:

Get-PSSnapin
Name : Microsoft.PowerShell.Core
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains Windows
PowerShell management cmdlets used to manage
components of Windows PowerShell.

Name : Microsoft.PowerShell.Host
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains cmdlets
used by the Windows PowerShell host.

Name : Microsoft.PowerShell.Management
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains management
cmdlets used to manage Windows components.

Name : Microsoft.PowerShell.Security
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains cmdlets to
manage Windows PowerShell security.

Name : Microsoft.PowerShell.Utility
PSVersion : 1.0
Description : This Windows PowerShell snap-in contains utility
Cmdlets used to manipulate data.

As you see, all cmdlets come from snap-ins. Even the tightly built-in cmdlets are not at all as tightly built-in as it seems. When PowerShell starts, it loads them from various snap-ins. So, it might be that some additional snap-ins are on your system, such as to manage Microsoft Exchange or to perform other tasks.

But Get-PSSnapin not only displays snap-ins that are already loaded, but any others as well. Using the -registered parameter instructs Get-PSSnapin to list all registered snap-ins. Since you already registered your own snap-in, it should be in this list:

Get-PSSnapin -registered
Name : Clipboard-Tool
PSVersion : 1.0
Description : Copy text to clipboard

Name : Pscx
PSVersion : 1.0
Description : PowerShell Community Extensions (PSCX) base snapin
which implements a general purpose set of cmdlets.

If you want to use a new snap-in, you have to load it using Add-PSSnapin. You need to do it once for every PowerShell session so, if you need the snap-in often, you should get it to automatically load right into one of your PowerShell profile scripts (see Chapter 10).

Add-PSSnapin Clipboard-Tool

As soon as the snap-in is loaded, all the cmdlets it contains will be available. You could call your new Out-Clipboard cmdlet to move text to the clipboard:

Out-Clipboard -text "Hi there"
Out-Clipboard "Hi there"

It may also be used inside the pipeline since your cmdlet supports the PowerShell pipeline. The next line will copy your output to the clipboard:

Dir | Out-Clipboard

However, if you insert text from the clipboard into a program like Notepad, you may ask why just the file name and not the complete directory listing are displayed.

The answer lies in the PowerShell pipeline, which, remember, transports objects. The result of Dir is individual objects, while Out-Clipboard is expecting text. That's why only one object property, the name, is passed onto the clipboard. If you really want to move the entire directory listing in the form in which it is normally displayed in the console to the clipboard, you should first use Out-String to convert the objects into text:

Dir | Out-String | Out-Clipboard

By the way, your new cmdlet also supports many other aspects of cmdlets like debugging messages. If you specify the -debug parameter, your cmdlet will output all the reports that were written to your source code using WriteDebug(). Depending on the debugging settings in $debugpreference, you can either have your computer ask you to confirm each step or just show yellow-colored debugging messages.

Out-Clipboard Hello -debug
DEBUG: Enter BeginProcessing

Confirm
Continue action?
|Y| Yes |A| Yes to All |H| Halt Command |S| Suspend |?| Help (default is "Y"): y

DEBUG: Processing Record

Confirm
Continue action?
|Y| Yes |A| Yes to All |H| Halt Command |S| Suspend |?| Help (default is "Y"): a

DEBUG: Done Processing Record

DEBUG: Enter EndProcessing

The Structure of Cmdlets

Now, let's look a little more closely at the source code of your cmdlet, which actually consists of three classes:

  • Snap-Ins: The first class defines the snap-in, that is, the general container for all following cmdlets.
  • Cmdlet: The second class defines the Out-Clipboard cmdlet.
  • Helper classes: Finally, the third class corresponds to clipboard functionality and is used by the cmdlet to copy text to the clipboard.

The Snap-In

The snap-in enables installation of the cmdlet package using the RunInstaller attribute. The installutil registration tool can automatically enter this snap-in in the registry.

The second task of the snap-in is to retrieve information about maker, version, and function of the package. The ClipboardSnapin class is derived from the PSSnapin prototype using Inherit so that this information can be called for every snap-in while always using the same properties. This enables the class to be assigned standard properties that the class later defines more precisely using Overrides.

<RunInstaller(True)> Public Class ClipboardSnapin
Inherits PSSnapIn

Public Sub New()
(...)
End Sub

Public Overrides ReadOnly Property Name() As String
(...)
End Property

Public Overrides ReadOnly Property Vendor() As String
(...)
End Property

Public Overrides ReadOnly Property VendorResource() As String
(...)
End Property

Public Overrides ReadOnly Property Description() As String
(...)
End Property

Public Overrides ReadOnly Property DescriptionResource() As String
(...)
End Property
End Class

The Cmdlet

The actual cmdlet is an additional class that is derived this time from the Cmdlet prototype. The Cmdlet attribute defines the name of the cmdlet. Only certain names are allowed as verbs because the names of all cmdlets obey strict naming rules. That's why the verb out comes from the VerbsData list, which sets the permitted name. In contrast, the noun part of the name can be freely selected and is specified as Clipboard. As a result, the complete name of this cmdlet is Out-Clipboard.

<Cmdlet(VerbsData.Out, "Clipboard")> Public Class ClipboardHelper
Inherits Cmdlet

There follow the parameters of the cmdlet. In this case, only one parameter called Text is set. Its position is 0, which means that if no parameter name is specified, Out-Clipboard will bind the first specified argument to the parameter. For this reason, you may type both Out-Clipboard -text Hello and Out-Clipboard Hello.

So that your cmdlet can also get input from the pipeline, ValueFromPipeline is set to True,making it possible for you to use your cmdlet inside the pipeline: Dir | Out-Clipboard.

<Parameter(Mandatory:=False, Position:=0, ValueFromPipeline:=True, _
HelpMessage:="Text to copy to Clipboard"), ValidateNotNullOrEmpty()> _
Public Property Text() As String()
Get
Return _Text
End Get
Set(ByVal value As String())
_Text = value
End Set
End Property

Begin, Process, End

In conclusion, you should specify what will happen when your cmdlet is called. Like the functions we saw in Chapter 9, three phases must be distinguished. BeginProcessing() is called when the cmdlet is activated (initialization). EndProcessing() is called when the cmdlet has ended its work (tidying tasks). And ProcessRecord() is called for every single object that is passed to the cmdlet.

Protected Overrides Sub BeginProcessing()
(...)
End Sub

Protected Overrides Sub EndProcessing()
(...)
End Sub

Protected Overrides Sub ProcessRecord()
(...)
End Sub

It is best for you to look at what happens in practice by calling your cmdlet using the -debug parameter, which enables you to read the debugging messages that are in your source text with WriteDebug() and that tell you exactly when each part was executed.


Posted Apr 10 2009, 05:41 PM by ps1

Comments

Chapter 20. Your Own Cmdlets and Extensions - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com wrote Chapter 20. Your Own Cmdlets and Extensions - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com
on 04-10-2009 5:46 PM

Pingback from  Chapter 20. Your Own Cmdlets and Extensions - Master-PowerShell | With Dr. Tobias Weltner - PowerShell.com

Concentrated Tech NSoftware Dell Compellent Sponsored by Idera and Concentrated Tech and NSoftware and Dell Compellent
Copyright 2011 PowerShell.com. All rights reserved.