Wednesday, May 11, 2011

Write your first powershell Cmdlet in C#

Once I get more organized in this whole blogging world, I may take a moment to elucidate my reasoning for even wanting to write a Powershell cmdlet in C#. But for now, I just wasted a morning in learning how to do it, and the internet just wasn't very helpful on getting started (including MSDN). So, for now I will just dive into the steps I eventually found, with the hope that someone else can use this information.

What I used:
 Powershell v2.0
Windows XP
Visual Studio 2010
Windows SDK

If you don't have the Windows SDK, it is apparently the way you can get the libraries for Powershell that will be needed for your Cmdlet project. So download and install that first. I used the version 6 flavor, though there are newer ones for more modern OS's than XP.

Fire up Visual Studio.
Create a new Class Library for C#. I named it PowershellCmdlet.

Replace the code with this sample from MSDN


using System.Management.Automation;
 
namespace PowershellCmdlets
{
 // Declare the class as a cmdlet and specify and 
 // appropriate verb and noun for the cmdlet name.
 [Cmdlet(VerbsCommunications.Send, "Greeting")]
 public class SendGreetingCommand : Cmdlet
 {
  // Declare the parameters for the cmdlet.
  [Parameter(Mandatory = true)]
  public string Name
  {
   get { return name; }
   set { name = value; }
  }
  private string name;
 
  // Overide the ProcessRecord method to process
  // the supplied user name and write out a 
  // greeting to the user by calling the WriteObject
  // method.
  protected override void ProcessRecord()
  {
   WriteObject("Hello " + name + "!");
  }
 }
}


You will notice that the only "using" statement is for "System.Management.Automation.  This is not part of the built in System.Management namespace so you will need to add in one of the libraries that got installed with the Windows SDK. It should install to:

C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll

(The keen observer will see that the folder name is "v1.0". What the? turns out there are a lot of places in the Powershell v2.0 folders are still labeled v1.0. Comment if you know why please.)

Add a reference to that library to your project.

Now you should be able to build your project and generate a .dll file of the same name.

Now where I wasted a bunch of time. I have a C# Cmdlet for Powershell, but how do I get the darn thing loaded.  It appears the old school way was to create a PSSnapin. Which required creating some more code to handle the "snapping in" and dealt with registry settings etc. Sub-Optimal. What I wanted was the new Modules for Powershell v2.0, which supposedly would make it a breeze! For a nice explanation of Modules vs. Snap-ins check out Thomas Lee's blog.

So here is the trick to releasing your shiny new .dll file as a Cmdlet Module.

First understand that their is a default path where Powershell will search for modules that is stored in the PSModules environment variable:


PS > get-content Env:\PSModulePath
C:\Documents and Settings\<yourlogin>\My Documents\WindowsPowerShell\Modules;C:\WINDOWS\system32\WindowsPowerShell\v1.0\M
odules\

(You can add your own directory to this path, but for simplicity I just left the defaults.)


It turns out that on a fresh install of Powershell you may not actually have a "Modules" sub-directory, if that is the case, go create one in your user directory at the end of the path given above.

Next create a subdirectory under your Modules directory, and here is the trick, Name it the same name as  your .dll file.  If the folder is a different name from your .dll name, it seems to not recognize the Module.

Now you need to create a Manifest Module for your .dll file. Powershell 2.0 provides a tool for this called "New-ModuleManifest".
Start this command with a path to your new PowershellCmdlet subdirectory you just created. It will ask you a series of cryptic questions that don't make much sense at first. Here is how you should answer them.


PS > New-ModuleManifest "C:\Documents and Settings\<yourlogin>\My Documents\WindowsPowershell\modules\PowershellCmdlets\something.psd1"

cmdlet New-ModuleManifest at command pipeline position 1

Supply values for the following parameters:
NestedModules[0]:
Author: Your Name
CompanyName: Your Company
Copyright: 2011
ModuleToProcess: PowershellCmdlets.dll
Description:
TypesToProcess[0]:
FormatsToProcess[0]:
RequiredAssemblies[0]:
FileList[0]:

PS >


Now it is supposed to be as simple as using the Import-Module command like so:


PS > import-module PowershellCmdlets
Import-Module : Could not load file or assembly 'file:///C:\Documents and Settings\<yourlogin>\My Documents\WindowsPowerShell\Modules\PowershellCmdlets\PowershellCmdlets.dll' or one of its dependencies. This assembly is built by a runtime newer than the currently loaded runtime and cannot be loaded.
At line:1 char:14
+ import-module <<<<  PowershellCmdlets
    + CategoryInfo          : NotSpecified: (:) [Import-Module], BadImageFormatException
    + FullyQualifiedErrorId : System.BadImageFormatException,Microsoft.PowerShell.Commands.ImportModuleCommand


What the?  I then went back to my Visual Studio and retargeted my solution to .net 3.5, then .net 2.0 with the same results.  Finally I found this article, which while not directly related to my problem actually provided the solution.

When Powershell runs it can read from a Powershell.exe.config file that can adjust some of the settings Powershell uses.  So to solve the "newer runtime" problem we can create a Powershell.exe.config file in the following directory:

C:\WINDOWS\system32\WindowsPowerShell\v1.0

With these contents:


<?xml version="1.0"?>
<configuration>
    <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v4.0.30319"/>
        <supportedRuntime version="v2.0.50727"/>
    </startup>
</configuration>


Restart your Powershell instance and you should be able to execute the following commands to run you Cmdlet.


PS > import-module PowershellCmdlets -verbose
VERBOSE: Loading module from path 'C:\Documents and Settings\<yourlogin>\My Documents\WindowsPowerShell\Modules\PowershellCmdlets\PowershellCmdlets.dll'.
VERBOSE: Importing cmdlet 'Send-Greeting'.

PS > Send-Greeting -name Bob
Hello Bob!




Hope this helps.













MSDN Starting point for cmdlet creation.

1 comment: