Click here to Skip to main content
15,886,857 members
Articles / Programming Languages / C#
Article

Writing an useful cmdlet for Windows PowerShell

Rate me:
Please Sign up or sign in to vote.
4.39/5 (9 votes)
7 Jul 20069 min read 58.2K   430   35   5
This article describes my findings while developing an integration of Copernic Desktop Search into Windows PowerShell.

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

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;
C#
[RunInstaller(true)]
public class GetFromCopernicSnapIn : PSSnapIn
{
    public GetFromCopernicSnapIn()
        : base()
    {
    }

    public override string Name
    {
        get
        {
            return "Get.FromCopernic";
        }
    }

    public override string Vendor
    {
        get
        {
            return "Joakim Mцller, Envious Data Systems HB";
        }
    }

    public override string Description
    {
        get
        {
            return "This snapin contains a cmdlet to" +
                   " search for items using Copernic Desktop Search.";
        }
    }
}

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;

Image 1

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;

C#
PS> "PoSh","rocks!" | Write-Host
PoSh
rocks!
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.

C#
PS> "PoSh","rocks!" | Measure-Object

Count    : 2
...
PS>

Creating Our Cmdlet Class

Our class definition will look like this;

C#
[Cmdlet(VerbsCommon.Get, "FromCopernic")]
public class GetFromCopernic : PSCmdlet

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.

C#
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.

C#
private string resultType = "files";

[Parameter(Mandatory = false, Position = 1),
ValidateSet(new string[] { "contacts", "emails",
     "favorites", "files", "history", "music",
     "pictures", "web", "videos" }, IgnoreCase = true)]
public string ResultType
{
    get { return resultType; }
    set { resultType = value; }
}

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.

C#
XmlDocument resultXml = new XmlDocument();
resultXml.LoadXml(searchResult);
XmlNodeList itemNodes = resultXml.SelectNodes("//Item");
foreach (XmlNode itemNode in itemNodes)
{
    WriteObject(itemNode);
}

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.

Image 2

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.

C#
private void WaitForResults()
{
    WriteVerbose("Waiting for results...");
    const int numSteps = 10;
    int sleepTime = timeOut / numSteps;
    for (int i = 1; i <= numSteps; i++)
    {
        WriteProgress(new ProgressRecord(0,
            String.Format(<BR>                  "Searching for \"{0}\" using Copernic Desktop Search...", <BR>                  query),
            String.Format("{0}% done", i * numSteps)));
        WriteDebug(String.Format("Sleeping for {0} ms.", sleepTime));
        System.Threading.Thread.Sleep(sleepTime);
    }
}

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.

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;

PS> Get-PSSnapin -registered


Name        : Get.FromCopernic
PSVersion   : 1.0
Description : This snapin contains a cmdlet to search for items using <BR>              Copernic Desktop Search.
As you will notice, the snapin is now ready to consume. To enable the cmdlets within, we need to run the Add-PSSnapin cmdlet.
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;

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;

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;

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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Sweden Sweden
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralOutputting FileInfo object instead of xml Pin
Sean McLeod28-Jun-06 5:30
Sean McLeod28-Jun-06 5:30 
GeneralRe: Outputting FileInfo object instead of xml [modified] Pin
Joakim Möller28-Jun-06 11:09
Joakim Möller28-Jun-06 11:09 
GeneralRe: Outputting FileInfo object instead of xml Pin
Sean McLeod28-Jun-06 21:57
Sean McLeod28-Jun-06 21:57 
GeneralRe: Outputting FileInfo object instead of xml Pin
Sean McLeod29-Jun-06 1:57
Sean McLeod29-Jun-06 1:57 
GeneralRe: Outputting FileInfo object instead of xml Pin
Joakim Möller29-Jun-06 20:26
Joakim Möller29-Jun-06 20:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.