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;
[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;
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;
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.
PS> "PoSh","rocks!" | Measure-Object
Count : 2
...
PS>
Creating Our Cmdlet Class
Our class definition will look like this;
[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.
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
.
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.
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.
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.
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.