Monday, June 26, 2006 6:41 PM bart

Windows PowerShell - registering a cmdlet without a custom shell

Some time ago I published an example of how to create a PowerShell cmdlet on my blog, "Windows PowerShell - Make your application manageable - Write your first cmdlet". As a reader pointed out, the creation of a custom shell is no longer required. At the time of writing the .NET Magazine article (Dutch), the PowerShell software hadn't reached the RC stage and I hadn't much time to mess around with the SnapIn-methodology, so it wasn't incorporated in the original article (we were happy we could get the MSH to PowerShell rename in the published article in time, as well as doing the required incompatibilies spotting to have the original code compile on the RC bits).

Today, I'm going to point out how to modify the code from the "Windows PowerShell - Make your application manageable - Write your first cmdlet" post (with the FindString cmdlet) to a SnapIn that can be registered in PSH shells on your machine, without the need for creating a new custom shell using make-shell. For the original post, see the link above. Below you'll find the stripped-down and updated version of this post using a snap-in approach.

Your first cmdlet (snap-in)

Step 1 - Create a Visual Studio project

To kick off, create a new class library project (e.g. in C#) and give it a meaningful name, for example FindString. Next, add a reference to the System.Management.Automation.dll assembly which you can find in %programfiles%\Windows PowerShell\v1.0. Include a reference to System.Configuration.Install as well.

Step 2 - Declare the cmdlet class

Next, it's time to declare our cmdlet-acting class as follows (updated using VerbsCommon, thx to Lee for his comment):

using System.Management.Automation;

[
Cmdlet(VerbsCommon.Get, "Matches"
)]
public class FindString :
PSCmdlet
{
}

The class has to inherit from PSCmdlet and has to be annotated with the CmdletAttribute attribute as you can see.

Step 3 - Parameterize the cmdlet

private string pattern;

[
Parameter(Mandatory=true
, Position=0)]
public string
Pattern
{
   get { return
pattern; }
   set { pattern = value
; }
}

private string
[] fullName;

[
Parameter(Mandatory=true, Position=1, ValueFromPipelineByPropertyName=true
)]
public string
[] FullName
{
   get { return
fullName; }
   set { fullName = value
; }
}

The first parameter is called Pattern and will contain the regular expression pattern we'll look for. The second one is called FullName and can contain more than one filename, which can be fed in through the pipeline by the property name. That means that cmdlets that return objects with a public property called FullName can be linked to this cmdlet by means of piping (|), e.g. from the get-childitem cmdlet.

Step 4 - Implement the core functionality

The core of the cmdlet's implementation goes in the ProcessRecord method. The following is a pretty short implementation of our matching logic relying on System.Text.RegularExpressions and System.IO to read files, and System.Collections.Generic for the List class:

protected override void ProcessRecord()
{
   List<Match> matches = new List<Match>();

   ProviderInfo
info;
   foreach (string item in FullName)
      foreach (string file in GetResolvedProviderPathFromPSPath(item, out info))
         using (StreamReader sr = File.OpenText(file))
            matches.Add(
new Match(file, Regex.Matches(sr.ReadToEnd(), pattern)));

   WriteObject(matches);
}

The call to WriteObject returns the result to the pipeline for further processing; other cmdlets could use our output as their input, or the user can just feed this in to cmdlets like format-list (or work with variables, etc). To aid our implementation, we've created a simple Match class:

public class Match
{
  
public Match(string file, MatchCollection
matches)
   {
     
this
.file = file;
     
this
.matches = matches;
   }

  
private MatchCollection
matches;

  
public MatchCollection
Matches
   {
     
get { return
matches; }
     
set { matches = value
; }
   }

   private string
file;

   public string
File
   {
     
get { return
file; }
     
set { file = value
; }
   } 
}

Step 5 - Create a snap-in and deploy it

Basically, a snap-in is a wrapper around one or more cmdlets that can be "imported" in a PowerShell instance. Instead of creating a separate shell (using make-shell) one can rely on snap-ins to bring custom cmdlets to Windows PowerShell.

The core of the snap-in methodology lies in the CustomPSSnapIn class from the System.Management.Automation namespace. This class inherits from System.ComponentModel.Component and thus it can be used in combination with instalers in .NET (using installutil.exe). That's how snap-ins work. In order to bring this technology to our own cmdlet-implementation, we have to do the following (with using directives for the whole codefile):

using System;
using System.ComponentModel;
using System.IO;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text.RegularExpressions;

#region
Snap-in

[
RunInstaller(true
)]
public class FindStringPSSnapIn :
CustomPSSnapIn
{
     private Collection<CmdletConfigurationEntry> cmdlets = new Collection<CmdletConfigurationEntry
>();
    
private Collection<ProviderConfigurationEntry> providers = new Collection<ProviderConfigurationEntry
>();
    
private Collection<TypeConfigurationEntry> types = new Collection<TypeConfigurationEntry
>();
     private Collection<FormatConfigurationEntry> formats = new Collection<FormatConfigurationEntry
>();

     public FindStringPSSnapIn() : base
()
     {
          cmdlets.Add(
new CmdletConfigurationEntry("get-matches", typeof(FindString), null
));
     }

     public override string
Name
     {
          get { return "FindStringPSSnapIn"
; }
     }

     public
override string Vendor
     {
          get { return "BdsSoft"
; }
     }

     public
override string Description
     {
          get { return "This snap-in contains the FindString cmdlet."
; }
     }

     public
override Collection<CmdletConfigurationEntry> Cmdlets
     {
          get { return
cmdlets; }
     }

     public override Collection<ProviderConfigurationEntry
> Providers
     {
          get { return
providers; }
     }

     public override Collection<TypeConfigurationEntry
> Types
     {
          get { return
types; }
     }

     public override Collection<FormatConfigurationEntry> Formats
     {
          get { return
formats; }
     }
}

#endregion

This might look quite some code at first glance, but it's really straightforward. All that you have to do over here is override from CustomPSSnapIn and provide code that returns a vendor, a name, a description and a few collections through properties. Those collections hold references to cmdlets, providers, types and format that are included in the snapin. In this case, we only include one single cmdlet which is done by preparing a Collection (from System.Collections.ObjectModel) of CmdletConfigurationEntry (from System.Management.Automation.Runspaces) instances. In our case on single entry is enough, holding the name (get-matches) and type info (typeof(FindString)). The last constructor parameter isn't used as we don't have a help file associated with the cmdlet.

Compile the whole stuff and open a Visual Studio 2005 Command Prompt (or a PS prompt if you're ambitious) and run installutil.exe against the compiled .dll file (note: on Vista, you'll have to evelate the Visual Studoo 2005 Command Prompt to run with administrative credentials in order to allow installutil.exe to write to the HKLM registry hive):

C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug>installutil.exe FindString.dll
Microsoft (R) .NET Framework Installation utility Version 2.0.50727.88
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\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.dll assembly's progress.

The file is located at C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.InstallLog.
Installing assembly 'C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.dll'.
Affected parameters are:
   logtoconsole =
   assemblypath = C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.dll
   logfile = C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.InstallLog

The Install phase completed successfully, and the Commit phase is beginning.
See the contents of the log file for the C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.dll assembly's progress.

The file is located at C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.InstallLog.
Committing assembly 'C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.dll'.
Affected parameters are:
   logtoconsole =
   assemblypath = C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.dll
   logfile = C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug\FindString.InstallLog

The Commit phase completed successfully.

The transacted install has completed.

C:\Users\Bart\Documents\Visual Studio 2005\Projects\FindString\FindString\bin\Debug>

Step 6 - Testing time

Launch the Windows PowerShell and display a list with registered snap-ins as follows:

PS C:\Users\Bart\Documents> get-pssnapin -registered


Name        : FindStringPSSnapIn
PSVersion   : 1.0
Description : This snap-in contains the FindString cmdlet.

As you can see, the snap-in was registered successfully. However, get-matches still doesn't work:

PS C:\Users\Bart\Documents> get-matches
'get-matches' is not recognized as a cmdlet, function, operable program, or script file.
At line:1 char:11
+ get-matches <<<<

To make it available, you have to call add-pssnapin, as displayed below:

PS C:\Users\Bart\Documents> add-pssnapin FindStringPSSnapIn

Now get-matches should work:

PS C:\Program Files\Windows PowerShell\v1.0> get-matches Wmi about_Alias.help.txt

Matches                                 File
-------                                 ----
{Wmi, Wmi, Wmi}                         C:\Program Files\Windows PowerShell\...

Step 7 - Taking it one step further

Performing add-pssnapin every time again doesn't boost your productivity. In order to provide easier access to your shell configuration you can export the settings using export-console:

PS C:\Users\Bart> export-console MyShell

This creates a file called MyShell.psc1 that contains the following piece of XML:

<?xml version="1.0" encoding="utf-8"?>
<PSConsoleFile ConsoleSchemaVersion="1.0">
  <PSVersion>1.0</PSVersion>
  <PSSnapIns>
    <PSSnapIn Name="FindStringPSSnapIn" />
  </PSSnapIns>
</PSConsoleFile>

You can now open your customized shell using powershell.exe by specifying the psc1-file as a parameter (or create a shortcut of course):

powershell -PSConsoleFile c:\users\bart\myshell.psc1

Have fun!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under:

Comments

# Getting started with Windows PowerShell - Your first cmdlet (webcast)

Wednesday, July 05, 2006 12:49 AM by B# .NET Blog

In this personal webcast episode called &quot;Creating your first Windows PowerShell cmdlet&quot;, I'm showing...

# C# 2.0 Iterators

Friday, July 07, 2006 12:28 AM by B# .NET Blog

Introduction
In my post about LINQ a couple of days ago, I promised to do a dive deep post on iterators...

# re: Windows PowerShell - registering a cmdlet without a custom shell

Thursday, July 13, 2006 6:32 AM by Jeffrey Snover

PSMDTAG:FAQ: Getting Started - How do I get started with my first Cmdlet?

PSMDTAG:SDK: Writing your first Cmdlet.

PSMDTAG:CMDLET: Add-PSSNAPIN, Export-Console


Jeffrey Snover [MSFT]
Windows PowerShell/Aspen Architect
Visit the Windows PowerShell Team blog at:    http://blogs.msdn.com/PowerShell
Visit the Windows PowerShell ScriptCenter at:  http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

# A graphical MessageBox confirmation cmdlet in PowerShell

Thursday, October 05, 2006 12:32 PM by B# .NET Blog

Introduction
I've been posting about Windows PowerShell programmability quite a bit lately. A few interesting...

# Newsgator PowerShell Provider is almost ready! | Aaron Lerch

Pingback from  Newsgator PowerShell Provider is almost ready! | Aaron Lerch

# PowerShell within an ASP.NET application

Monday, November 05, 2007 12:42 AM by Coding Obsession

PowerShell within an ASP.NET application

# Windows PowerShell 2.0 Feature Focus - Script cmdlets

Saturday, March 22, 2008 9:09 AM by B# .NET Blog

Two weeks ago I did a little tour through Europe spreading the word on a couple of our technologies including