Writing an useful cmdlet for Windows PowerShell

May 30, 2006

9 min read

.NET2.0

Vista

VS2005

C#

Windows

.NET

Visual-Studio

Dev

COM

Intermediate

Author picture

by Joakim Möller

Contributor

59k Views

Introduction

I am very fond of the Copernic Desktop Search (CDS) application. Really, the invention of desktop search applications have changed the quality of my work dramatically. This is not a discussion about desktop search though. It is all about Windows Powershell, the latest invention from Microsoft to make us all stay faithful to the Windows world.

Windows Powershell

Windows Powershell, previously known as Microsoft Shell (MSH), codenamed Monad and now abbreviated PoSh (I spot a lawsuit from Mrs Beckham coming!) is a complete replacement for the Command Prompt and Windows Scripting Host. I won't go into the full details here since they are well known by now, but in short it is an object oriented shell that allows for pushing around objects instead of text. The advantage of this is quite obvious. No longer do we have to scrape screen text to find the interesting pieces, we just select out what we want using named properties.

A Powerful Example

TEXT
PS> ((get-date) - [DateTime]([xml](new-object Net.WebClient).DownloadString(
 "http://blogs.msdn.com/powershell/rss.aspx")).rss.channel.item[0].pubDate).Days
18
PS>

Pheew. That is probably not something you would write every day, but I chose it just to demonstrate where the "Power" in Powershell originates from. That single line above actually downloads the latest feed from the PoSh team blog and calculates the amount of days that have passed since the last post. When writing this they lagged 18 days, which is what we see as the final output.

I think you get the point.

Snapins and Cmdlets

A snapin might be thought of as a collection of cmdlets, pronounced command-lets, small code snippets that each provide a piece of functionality. new-object and get-date above are two such cmdlets. What I wanted to do is to extend PoSh with one such cmdlet, which brings us to the topic of this article.

Embrace and Extend

As I said previously, I like CDS. However, I do not like to have to use the GUI to perform searches. A year ago I spent some time browsing through the COM objects and stumbled upon the public CDS API. In the CopernicDesktopSearchLib there are several interfaces provided to access the search engine using script. The two methods of interest for us are ExecuteSearch and RetrieveXMLResults of the ICopernicDesktopSearch interface. ExecuteSearch starts an asynchronous search process and the results are later available through the RetrieveXMLResults method. Unfortunately for us, as you will see, there are no provided methods or events to know when the results are available in CDS 1.7. To work around this, I had to add a less-than-nice solution where the application sleeps for a few seconds while the results build up. In CDS 2.0, currently in beta, there are additional interfaces to query when the search is completed and I will update this article as soon as version 2.0 is released.

First Steps

So what do we do with this knowledge? First, fire up Visual Studio 2005 and create a new class library project. This project type will build a DLL for us that we will feed to PoSh later on.

Start by adding the three references that we are dependent on;

  • Copernic Desktop Search Library
    This is found in the COM tab of the Add References dialog after installing CDS. Visual Studio will automatically create the necessary COM Interop for us.
  • System.Management.Automation
    This library is contained within the file System.Management.Automation.dll which you will find in the PoSh installation folder.
  • System.Configuration.Install
    You will find this in the .NET tab of the Add References dialog.
The System.Management.Automation library contains all PoSh related interfaces and enumerations. The System.Configuration.Install library is used by the installation logic to automatically make our snapin available for PoSh. This is done by adding the following code;
CS
    [RunInstaller(true)]
    public class GetFromCopernicSnapIn : PSSnapIn
    {
        public GetFromCopernicSnapIn()
            : base()
        {
        }

        public override string Name
        {
            get
            {
                return 

As you see, all of the logic is provided for us in the PSSnapIn base class, we only override the properties that are specific for our project and mark the class with the RunInstaller attribute.

Building the Basics

A typical cmdlet contains of a class inheriting from PSCmdlet marked with the Cmdlet attribute and at least one of the BeginProcessing(), ProcessRecord() and EndProcessing() overridden methods. While processing the input from the pipeline, the PoSh framework will call these methods according to the following collaboration diagram;

Article image

As you can see, you will first get one call to BeginProcessing() followed by one or multiple calls to ProcessRecord() and finally a single call to EndProcessing(). Consider the following example;

CS
PS> 

Here you pass an array of strings to the Write-Host cmdlet. In this case, Write-Host will receive two calls to ProcessRecord(), one for the string "PoSh" and one for "rocks!". The Write-Host cmdlet is designed to output each string to the console as served.

The opposite of this would be a command that consumed all input and only emitted data in EndProcessing(). An example of this is the Measure-Object cmdlet. Here, all object information is silently consumed in ProcessRecord() and a summary is emitted in the EndProcessing() method.

CS
PS> 

Creating Our Cmdlet Class

Our class definition will look like this;

CS
    [Cmdlet(VerbsCommon.Get, 

The VerbsCommon class is one of six predefined enumerations of standard verbs. The others being VerbsCommunications, VerbsData, VerbsDiagnostics, VerbsLifecycle and VerbsSecurity. The PoSh developers have decided on a naming scheme of verb-noun, where you get to choose the noun. In our case we are retrieving something from CDS, hence we use the get verb.

Adding Interactivity

To interact with the cmdlet you add public properties to the class and mark them with the Parameter attribute. A very special parameter is the pipeline value, the data that arrives to our cmdlet as the output from another cmdlet.

CS
        private string query;

        [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true),
        ValidateNotNull()]
        public string Query
        {
            get { return query; }
            set { query = value; }
        }

The Parameter attribute is used to promote this property as a cmdlet parameter. The Mandatory parameter is set to true since we cannot do much without a search query. The ValueFromPipeline parameter tells PoSh that we want to receive the pipeline data in this property.

Now we are able to receive the query we want to execute. We also need a way to specify the type of query since CDS are able to search from a variety of providers, including files and e-mails. We do this by adding another property called ResultType.

CS
        private string resultType = 

You notice the ValidateSet attribute? PoSh provides a number of built-in validators to ensure that the correct arguments are given to your cmdlet. In this case we want the user to choose from a set of tabs (providers) in CDS.

Adding Logic

Finally we will wrap this up by adding a few lines of code to actually perform the search. We need to take the querystring which was provided to us through the Query property and feed it to the ExecuteSearch method. We then wait a few seconds for the results to build up and retrieve the results using RetrieveXMLResults. Nothing of this is PoSh-specific so I will not bore you with the details here, you will find them in the source code zip.

Since this cmdlet might be one of many in the pipeline, we need to write back our results to the other end of the pipe. We do this using the WriteObject() method. Since PoSh handles objects very well, we do not have to take any special care other than to convert the resulting XML string to XML nodes for everyone's conveniance. This is done so that we can directly access the XML elements of the output later on.

CS
        XmlDocument resultXml = new XmlDocument();
        resultXml.LoadXml(searchResult);
        XmlNodeList itemNodes = resultXml.SelectNodes(

Showing progress

Since version 1.7 of CDS does not support any means of notification, events or status polling to see whether a search is completed or not I use a simple timer. To indicate a lengthy process for the user, PoSh provides a command called WriteProgress that brings up a graphical progress bar in the console.

Article image

To implement this, I added a method called WaitForResults that simply gets the value from the TimeOut parameter and sleeps in small steps to be able to show progress.

CS
        private void WaitForResults()
        {
            WriteVerbose(

Test Drive

Build the snapin project and open the Visual Studio 2005 Command Prompt. Navigate to the debug output folder and run the InstallUtil command on the DLL to register it with PoSh.

TEXT
   installutil Get.FromCopernic.dll

This could of course be done using PoSh as well, but for the sake of simplicity I chose this approach since all environment paths are automatically set up for you.

Adding the Snapin

From within PoSh, execute the following cmdlet;

TEXT
PS> Get-PSSnapin -registered


Name        : Get.FromCopernic
PSVersion   : 1.0
Description : This snapin contains a cmdlet to search for items using 
As you will notice, the snapin is now ready to consume. To enable the cmdlets within, we need to run the Add-PSSnapin cmdlet.
TEXT
PS> Add-PSSnapin Get.FromCopernic

Excited? You should be. If everything went well so far, you should now be able to run the following command;

TEXT
PS> Get-FromCopernic "Microsoft" | select Url

Url
---
C:\...
C:\...
C:\...

Most probably you will now be presented with a list of paths to the first ten files containing the word "Microsoft". What you implicitely just did was to provide the System.String "Microsoft" to the parameter Query of the cmdlet Get-FromCopernic that you just built. Congratulations!

Advanced Usage

I guess you are a bit curious about the more advanced uses of this cmdlet. What about the ResultType property? Try executing the following;

TEXT
PS> Get-FromCopernic "Microsoft" -resulttype "Foo"
Get-FromCopernic : Cannot validate argument "Foo" because it does not 
                   belong to the set "contacts, emails, favorites, 
                   files, history, music, pictures, web, videos".
At line:1 char:41
+ Get-FromCopernic "Microsoft" -resulttype  <<<< "Foo"

Wow. What happened? Since we provided an invalid argument, that was not included in the set we defined in the ValidateSet attribute, we get an error. The error is kind enough to provide the valid result types for us.

So let us actually use this cmdlet for something. Imagine for example that you want to know how many of the documents in the current folder that you have sent or received through e-mail. You execute the following;

TEXT
PS> Get-ChildItem *.doc | Get-FromCopernic -resulttype "emails" | Measure-Object

Count    : 11
Average  :
Sum      :
Maximum  :
Minimum  :
Property :
In my case, I had 11 hits. This is really powerful, I think!

Verbose and Debug Logging

Want more detail about what is happening behind the scenes? Throughout the cmdlet I have called the methods WriteDebug and WriteVerbose. These are very useful during testing. To enable debug logging, you set the variable $DebugPreference to "Continue". When enabled, all debugging information submitted from the code will be written to the console. To disable again you set the variable to "SilentlyContinue". The same goes for verbose logging but through the $VerbosePreference variable.

Rounding Up

I hope you have got some inspiration about how to create your own snapins and cmdlets using this information. Every day I learn something new that improves my ability to manage information. I am convinced that the future for Windows managebility is leveraged through PoSh.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)