Article Overview
In this article we will take a look at how to write a custom cmdlet for Windows PowerShell, which is the new command shell from Microsoft (formerly called "Monad"). Our cmdlet named "Get-Contact" will have the ability to retrieve contacts defined in Outlook 2007.
The full source code, inlcuding unit tests, installations scripts etc. can be found here.
The contents of this post is as follows:
- In the first section, we will take a quick look at PowerShell. We look at the main drivers behind it's creation and what some of the key factors are that differentiate PowerShell from other well-known shells and scripting environments.
- In the second section,we tell you how to download PowerShell if you don't have it installed yet (shame on you! ;-), and take a quick look at the PowerShell Extensions (PCX) library, which is an open-source project for writing custom PowerShell extensions.
- Next we take a look at PowerShell cmdlets, we'll show some examples, and discuss Microsoft's naming standards.
- Once we have a good understanding of cmdlets, we talk about the PowerShell SDK, and the PowerShell extension templates, which allow you to quickly get the "shell" (pun intended ;-) of a custom cmdlet up and running quickly.
- Before we start the implementation of our cmdlet, I thought it would be a good idea to list the detailed requirements of our Get-Contact cmdlet, and lists its parameters and arguments.
- Since we will be interacting with Microsoft Outlook 2007, we will make sure that you have the new Outlook 2007 Primary Interop Assembly installed. If not, I will walk you through the process of downloading and installing the PIA.
- Once we have all these pre-requisites out of the way, we will finally be able to start writing the code of our cmdlet.
- Finally, we'll write some scripts that test the functionality of our new cmdlet.
That quite a long list that we have here, so let's get rolling!
Microsoft PowerShell Overview
Windows PowerShell is the new command line and scripting language for Windows environments. While it has been designed and optimized for Windows, it is based on a rich shell heritage that stretches all the way back to the original Unix shells such as "csh", "ksh", and the more recent "bash" (the "Borne Again SHell"). Jeffrey Snover (the Windows PowerShell architect) and Bruce Payette (the lead designer of the language) both have a very extensive dynamic language background, and have been involved in the creation of a variety of command shells.
In this book "Powershell in Action" (which I highly recommend), Bruce Payette states that the overall goal of the PowerShell project was to provide the best shell scripting environment possible for Microsoft Windows.
I have yet to find somebody who really loved the old Windows Command line (cmd.exe). Maybe I should check some mental health institutions, because that's where some folks could have ended up if they tried to do some advanced scripting tasks with cmd.exe. All joking aside, as Bruce says in his book, the focus of Windows was always on the GUI and the average user, and not necessarily the computer professional and/or the systems administrator.
Initially, Microsoft created GUI tools (such as mmc.exe) to administer desktops and servers. But now that the power of the PC has increased, Windows is used in the corporate data center as well as on the user's desktop. In such a data center, the graphical point-and-click management approach that worked well for one machine does not quite scale. This limitation illustrates the need for a powerful, command-line based scripting environment.
The starting point for the PowerShell language was the grammar for the POSIX standard shell defined in IEEE specification 1003.2. One of the main enriching features that Microsoft added to this starting point was support for object-orientation. Indeed, PowerShell is the first fully object-oriented shell out there. I think that everybody is familiar with the typical "command pipeline" for command shells. In traditional scripting environment, only text is flowing through the pipeline from one command to the next. In PowerShell we have OBJECTS flowing between commands, so none of the semantics of the objects produced and consumed on either end of the pipeline are lost.
This also implies that PowerShell is fully compatible with the .NET type system. Actually, the .NET object model IS the object model in PowerShell, eliminating any impedance mismatch between the two. Because of this, PowerShell can leverage the intellectual capital captured in the vast universe of .NET code, including the .NET Base Class Library (BCL), and any other third-party .NET code. PowerShell also fully supports COM interoperability, ensuring us that we will be able to leverage any past investments in the COM and COM+ arena.
Installing PowerShell and the PCX Extensions
Windows Server 2008 will ship with PowerShell "in the box". You will also have PowerShell installed on any server that has a product installed that leverages PowerShell for it's administration implementation, such as:
- Exchange Server 2007.
- VMM (Virtual Machine Manager).
- Desktop Protection Manager.
Otherwise, you should first download PowerShell from this site. Running the installer is very straightforward, and only takes a couple of minutes. After the installation completes successfully, you can start an interactive PowerShell session as follows:
Start -> Programs -> Windows PowerShell
You also would want to download the Windows PowerShell 1.0 documentation pack. It contains several documents ("Release Notes", a one page "Quick Reference" etc), but what I like the most is the "Windows PowerShell Primer", which is about 100 pages, and a good place to get started with PowerShell.
Next, I recommend that you download the PowerShell Community Extension (PCX) from CodePlex. This is a very useful set of cmdlets, providers, aliases, filters, functions and scripts that members in the beta-testing community had expressed interest in, but never made it into PowerShell v1.0. The functionality covers a wide number of areas, including:
- Compression (zip, tar etc.) functionality.
- A wide variety of XML extensions.
- Clipboard functionality.
- SMTP support.
- Terminal Session support.
PowerShell Cmdlets
A cmdlet is basically a lightweight command in the PowerShell language. I added the term "lightweight" here, because a cmdlet will NOT create a new process or AppDomain. Instead it runs in the default AppDomain of the PowerShell process. When you compare a cmdlet to a "traditional shell command", such as a command in the Unix "C Shell (csh)", you will notice that they differ in the following ways:
- Cmdlets are instances of .NET classes, not stand-alone executables. This also implies that when we execute a cmdlet, we do not have to incur the overhead of process creation.
- Cmdlets are very easy to write in any .NET language.
- Cmdlets will typically not do their own parsing, error presentation or output formatting. Parsing, error presentation, and output formatting are handled by the Windows PowerShell runtime. This also guarantees consistency and ease of integration into new scripts. The advantages of this cannot be overstated. When you implement your custom cmdlet (as we will do shortly), you can use simple attributes to identify a property as a cmdlet argument, define whether it's a positional or named arguments, state that the parameter should support wildcards etc., without the need to do any extra programming.
- Cmdlets process input objects from the pipeline rather than from streams of text, and they typically deliver objects as output to the pipeline. This goes back to the fact that PowerShell is really a fully object-oriented shell.
- Cmdlets are record-oriented, processing a single object at a time. The PowerShell pipeline supports streaming, which improves performance, and limits user wait times, because the command on the right side of the pipeline can start processing the records produced by the left-side command as soon as they become available.
PowerShell uses a verb-noun pair for the names of cmdlets. For example, the Get-Process cmdlet built into PowerShell is used to retrieve a list of all processes that are current running on the host computer. The verb part ("Get") of the name identifies that action that the cmdlet performs. The noun part ("Process") of the name identifies the entity on which the action is performed (in this case, one or more Windows Processes).
Microsoft recommends that you follow the following naming rules when creating a new cmdlet:
- When specifying the verb part of the name, it is strongly recommended that you use one of the predefined verb names provided by Windows PowerShell (as defined by the VerbsCommon enumeration). If you use these standards names, you will ensure consistency between the cmdlets that you create, and those provided by Microsoft, or third-parties, such as PCX.
- Use only the present tense of a verb for a name. For example, use "Set" instead of "Setting".
- To enforce consistency, do not use a synonym of an approved verb name. (or in other words, don't get creative ;-)!
- Always use a simple, singular noun in cmdlet naming. For example, use "Get-Database" instead of "Get-Databases". While this might seem a bit strange at first it makes sense, because cmdlets might return either a single object or a collection, and you don't want to use different cmdlet names for both situations. So, Microsoft selected the singular name and used it consistently.
- Use Pascal Casing for both verb and noun names.
The verb names page of the PowerShell SDK lists the common verbs defined in the VerbsCommon enumeration.
The PowerShell SDK
Ensuring that you have the SDK installed
The PowerShell SDK is an integral part of the Windows SDK. It consists out of a class library, documentation and tools which allow you to create:
- Custom cmdlets (which is the topic of this article).
- Custom PowerShell Providers (a PowerShell Provider allows you to navigate or browse a set of stored data, using a consistent set of cmdlets such as Get-Item, Set-Item, Copy-Item, Get-ChildItem etc.). This is something we might touch on in a future article.
- Custom PowerShell hosts. Your application can act as a host to PowerShell, and invoke command pipelines directly from the host. This is the approach used by the admin UI of applications such as Exchange 2007.
- Create custom PowerShell Runspaces. Runspaces provide the mechanisms that hosting application use to execute pipelines in a well-constructed, consistent manner, and are often used by custom PowerShell hosts.
If you have downloaded the Windows SDK, you will have the PowerShell SDK installed. If you don't have the Windows SDK installed, well, what are you waiting for? ;-). You can download the Windows SDK for Windows Vista and the .NET framework 3.0 from this location.
After installation, you can find a large number of samples in the directory (if you used the standard installation directory):
C:\Program Files\Microsoft SDKs\Windows\v6.0\Samples\SysMgmt\WindowsPowerShell
Examples for each of the categories of custom objects are included in this directory.
The Windows PowerShell Extension Templates for Visual Studio
These Visual Studio templates add a C# and VB.NET project template for building Windows PowerShell cmdlets and providers. Using these templates will save you a lot of startup time, they will add the correct references to your project, create a skeleton for your custom cmdlet or provider, and add a generated PSSnapIn class for you to your project (we will talk more about PSSnapIn classes in the next section). After you download and unzip the files, you will get a .vsi file for C# and VB.NET, as is shown below:

Run the template of your choice, for example the C# template. You will see the following selection screen:

Leave all options selected. After you click Next, you will get a warning dialog, informing you that the content of the install package is not signed:

Click Yes, and the install will continue. In the next screen, click Finish to the install the template:

The install will complete and we'll be ready to start coding. Click Close to close the install dialog box:

After the installation has been completed, you will now have a PowerShell option in the New Project dialog:

The Get-Contact cmdlet - Detailed Requirements
The Get-Contact cmdlet should retrieve the list of contact defined in the local instance of Microsoft Outlook 2007. If no parameters are specified, it should return the entire list of contacts. The instances returned should be of type Microsoft.Office.Interop.Outlook.ContactItem. This type is defined in the Microsoft Outlook 2007 PIA.
The user should be able to restrict the list of contacts returned by last name. A LastName parameter should be provided for this purpose. This parameter should be able to accept a single argument, or a list of arguments, as shown in the examples below:
13# Get-Contact -LastName Hensel | Format-Table FirstName, LastName, CompanyName -AutoSize
FirstName LastName CompanyName
--------- -------- -----------
Brian Hensel Statêra
14# Get-Contact -LastName Hensel, Banker, Felker | Format-Table FirstName, LastName, CompanyName -AutoSize
FirstName LastName CompanyName
--------- -------- -----------
Brian Hensel Statêra
Chris Banker Statêra
Donn Felker Statêra
The LastName parameter should be positional and optional, so the previous command could also be written as follows:
15# Get-Contact Hensel, Banker, Felker | Format-Table FirstName, LastName, CompanyName -AutoSize
FirstName LastName CompanyName
--------- -------- -----------
Brian Hensel Statêra
Chris Banker Statêra
Donn Felker Statêra
The LastName parameter should also be able to accept wildcards. The syntax should be in accordance to the PowerShell wildcard specification. Below are a number of examples:
24# Get-Contact B* | Format-Table FirstName, LastName, Email1Address -AutoSize
FirstName LastName Email1Address
--------- -------- -------------
Eric Boocock /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=eboocock
Malcolm Boswell /o=STATORG/ou=FIRST ADMINISTRATIVE GROUP/cn=RECIPIENTS/cn=MBOSWELL
Chris Banker /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=cbanker
Tony Blodgett /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=tblodgett
25# Get-Contact [CE]* | Format-Table FirstName, LastName, Email1Address -AutoSize
FirstName LastName Email1Address
--------- -------- -------------
Jeremy Campbell /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=jcampbell
Erl Egestad /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=eegestad
Julie Clint /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=jclint
Mike Citro /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=mcitro
26# Get-Contact [m-x]* | Format-Table FirstName, LastName, Email1Address -AutoSize
FirstName LastName Email1Address
--------- -------- -------------
Matthew Ortiz /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=mortiz
Kyle Sanford ksanford@statera.com
Wayne Macdonald /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=wmacdonald
Bob Mills Bob_Mills@isagenix.net
Pete Miller /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=pmiller
Jon Robinson jRobinson@statera.com
From the above list, you also notice that the wildcard expansion should be case-insensitive.
The Outlook 2007 Primary Interop Assembly
The Microsoft Office Outlook Primary Interop Assembly (PIA) allows a developer to write managed applications for Microsoft Office Outlook 2007. The name of the assembly is Microsoft.Office.Interop.Outlook.dll. To check if you already have this assembly installed in the GAC, open up File Explorer, and navigate to %WINDIR\Assembly, and check if the assembly is already loaded in the GAC:

Or, for the hard-core PowerShell fans, you can use the following command:
51# dir $env:windir\assembly\gac\Microsoft.Office.Interop.Outlook
Directory: Microsoft.PowerShell.Core\FileSystem::C:\Windows\assembly\gac\Microsoft.Office.Interop.Outlook
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 12/16/2006 5:58 AM <DIR> 12.0.0.0__71e9bce111e9429c
(note how we use the $env:windir environment variable to refer to the windows directory, this is basically equivalent to using %WINDIR% in cmd.exe, but of cour much "cooler" ;-).
If you DO NOT see see the Outlook PIA using the mechanisms illustrated above, follow the instructions listed below to install it (note: these steps are for Windows Vista, but should be very similar for Windows XP):
- Bring up Control Panel. In Control Panel, select Programs. In the next screen, select Programs and Features, and from the list, right-click Microsoft Office Outlook 2007, and click Change as is shown below:

- In the next dialog, click Add or Remove Features, and click Continue.
- In the next screen, make sure that you select ".NET Programmability Support" as shown below:

- Click Continue. The Configuration Progress dialog will show the installation progress.
- Click the Close button in the Installation Completed screen.
Implementing our Cmdlet
Creating the Cmdlet skeleton with the Visual Studio Template
To create the skeleton for your custom cmdlet, follow these steps:
- Start Visual Studio, and select File | New Project. Select a location, and in the Project Types tree, select Windows PowerShell. Pick a name for your project, in my case I selected the name FooTheory.GetContactCmdlet as is shown below:

- Click OK to create the project. The template will create a class library with the appropriate references added, and a snap-in class, as shown in the screen shot below:

The PSSnapin.cs file implements a PowerShell Snap-in. Windows PowerShell snap-ins provide a mechanism for registering cmdlets and/or providers with the shell, thus extending the functionality of the shell. A Windows PowerShell snap-in can register all the cmdlets and providers in a single assembly, or it can register a specific list of cmdlets and providers. In our case, we will only have our one Get-Contact cmdlet in assembly, so we will only need to register that specific cmdlet.
Since I called my assembly Footheory.GetContactCmdlet, the template named my snap-in class FooTheory.GetContactCmdletSnapIn, which is of course an invalid class name, so I changed it to just GetContactCmdletSnapIn, and I renamed its source file from PSSnapIn.cs to GetContactCmdletSnapIn.cs. The methods of the Snap-in provide information to the installer, such as the Name of the assembly, the Vendor etc. The wizard does a pretty good job of implementing these methods, so you only need to provide some tweaks, like filling in the name of the Vendor etc. The finalized version of my Snap-in class is shown below:
1 using System;
2 using System.Management.Automation;
3 using System.ComponentModel;
4
5 namespace FooTheory.GetContactCmdlet
6 {
7 /// <summary>
8 /// This is our Snap-In class implementation. This class is
9 /// used by the Installer, and by the Add-PSSnapIn command
10 /// when we register our cmdlet with the PowerShell runtime
11 /// </summary>
12 [RunInstaller(true)]
13 public class GetContactCmdletSnapIn : PSSnapIn
14 {
15 public override string Name
16 {
17 get { return "FooTheory.GetContactCmdlet"; }
18 }
19 public override string Vendor
20 {
21 get { return "www.FooTheory.com"; }
22 }
23 public override string VendorResource
24 {
25 get { return "FooTheory.GetContactCmdlet,"; }
26 }
27 public override string Description
28 {
29 get { return "Registers the CmdLets and Providers in this assembly"; }
30 }
31 public override string DescriptionResource
32 {
33 get { return "GetContactCmdlet,Registers the CmdLets and Providers in this assembly"; }
34 }
35 } // class GetContactCmdletSnapIn
36 }
37
Notice also that the wizard added the following references to our project:
- System.Management.Automation. This is the main PowerShell SDK assembly
- System.Configuration.Install: Needed by the Snap-in for installation.
(the other assemblies are the "standard" assemblies added to any project).
At this point, I would recommend building your project to make sure that you have no compilation errors.
Adding our Cmdlet class
Now, we are ready to add our cmdlet class itself. Right-click the project and select Add | New Item. In the New Item dialog, you will notice that you have a choice of deriving from two different types of base classes:

- The first option (Windows PowerShell Cmdlet), will derive our cmdlet from the System.Management.Automation.Cmdlet base class. Deriving from this class means that our cmdlet is using a minimum set of dependencies on the Windows PowerShell runtime. This has two benefits:
- The footprint of our cmdlet will be smaller and if there are any changes in the Windows PowerShell runtime, our cmdlet will very likely not be impacted.
- It is also a lot easier to unit-test a cmdlet that is derived from the standard Cmdlet class, since we can directly create an instance of it and invoke it from our test code, without having to test it from the PowerShell command line.
- The second option (Windows PowerShell PSCmdlet) will derive our cmdlet from the System.Management.Automation.PSCmdlet class. If you derive from this class, you will have more direct access to the Windows PowerShell runtime environment. For example, from your cmdlet you can:
- Call scripts.
- Access Providers
- Access the current session state.
But this will also increase the size of your cmdlet, and it will tie our cmdlet more tightly to the current version of the PowerShell runtime.
In general, Microsoft recommends that you derive a custom cmdlet from the Cmdlet class, unless you absolutely have a need access to the PowerShell runtime. We have no such requirement, so we will derive our cmdlet from the System.Management.Automation.Cmdlet class. Select Windows PowerShell Cmdlet, and enter GetContactCmdlet.cs as the name for the source file, and click the Add button. This will add the GetContactCmdlet class to our project. This class contains the initial skeleton code for our cmdlet, which we will be customizing in our next section.
Customizing the Cmdlet Attributes
The default implementation of our cmdlet declares our class as follows:
namespace FooTheory.GetContactCmdlet
{
[Cmdlet(VerbsCommon.Get, "GetContactCmdlet", SupportsShouldProcess = true)]
public class GetContactCmdlet : Cmdlet
{
...
}
The CmdLetAttribute identifies a .NET class as a cmdlet and specifies the verb and noun pair used to invoke the cmdlet. The required parameters for this attribute include the following:
- VerbName. This is the name that describes the action performed by the cmdlet. Windows PowerShell strongly recommends that cmdlets use only the verb names specified by the action name enumerators (VerbsCommon). Note that in our case, we are using VerbCommons.Get, which indicates that we are retrieving information.
- NounName. This string is the name that describes the entity on which the cmdlet performs its action. The wizard will set this to the class name of our cmdlet, which in this case is not what we want. Since we are retrieving contact, we will use the string "Contact" here.
The optional SupportsShouldProcces named parameter indicates that our cmdlet supports calls to the ShouldProcess method, which provides the cmdlet with a way to prompt the user before an action that changes the system is performed. This is actually a very useful parameter, and I recommend that you support it in any of your cmdlets that you implement which modify the state of a system resource. It can avoid one of those "Oh Sh#$" situations that you could run into otherwise (I did not mean to do that, Oh my God, I'm fired etc.. ). The ShouldProcess method is triggered by using the confirm switch parameter on a command, as is shown below for the Stop-Process cmdlet:
13# Stop-Process 2920 -confirm
Confirm
Are you sure you want to perform this action?
Performing operation "Stop-Process" on Target "wmpnscfg (2920)".
Yes
Yes to All
No [L] No to All
Suspend [?] Help (default is "Y"):
Another very useful named parameter is ConfirmImpact. ConfirmImpact will trigger the cmdlet to show what action would be performed if the user would execute the command, without actually performing the action. It is triggered by the WhatIf switch parameter, an example with the Stop-Process cmdlet is shown below:
15# Stop-Process 2920 -whatif
What if: Performing operation "Stop-Process" on Target "wmpnscfg (2920)".
As you can see, the WhatIf switch parameter shows us the action(s) that would be trigger by the execution of the command without actually executing it. I really recommend using this switch for any command that you just not 100% sure about ; -)..
Back to the Get-Contact cmdlet now. In the Get-Contact cmdlet case, we are only retrieving information, and we are not changing anything in the state of the system, so we can safely set the value of the ShouldProcess member to false.
Our final class declaration for our cmdlet will look as follows:
1 namespace FooTheory.GetContactCmdlet
2 {
3 [Cmdlet(VerbsCommon.Get, "Contact", SupportsShouldProcess = false)]
4 public class GetContactCmdlet : Cmdlet
5 {
6 ....
7 }
8 }
Implementing a Record Processing Methodology
For a cmdlet to participate in the Windows PowerShell environment, it must override the ProcessRecord() method listed below, and optionally the BeginProcessing() and EndProcessing() methods. The semantics of these methods are shown below:
We will leverage these methods as follows:
- In BeginProcessing, we will setup the connection with Outlook, creating the main Outlook Application class.
- In ProcessRecord, we will retrieve the Outlook 2007 contacts, based upon the specified parameters, as defined in our list of requirements.
- In EndProcessing, we will disconnect from Outlook 2007, freeing up any tied-up COM objects from the Outlook PIA.
I don't like to mix the "core" cmdlet code and the Outlook code, so in my project structure I created an Implementation folder, and added a GetContactsImpl.cs class, which will contain our "real" implementation, the interactions with the COM objects etc. This class is marked as internal, and contains three methods to correspond with the main cmdlet processing methods:
- SetupOutlookConnection(): This method is called from our cmdlet's BeginProcessing method, and will initialize the connection to Outlook.
- GetContacts(): This method retrieves the actual contacts from outlook, and is called from the cmdlet's ProcessRecord method.
- TeardownOutlookConnection(): This method is called from the cmdlet's EndProcessing method, and cleans up out Outlook application object, and any other tied-up COM objects.
The following class diagram shows the relationship between our cmdlet and it's implementation class:

Creating a Unit Testing Framework for our Cmdlet
Now that we have the skeleton of our cmdlet all setup, it would be a good idea to create a unit test project for it. That way, we can quickly test the functionality of the cmdlet right in Visual Studio without having to go the PowerShell console to perform the testing.
To add the Unit Test project, follow these steps:
- Right-click the solution, and click Add | New Project.
- Under the Visual C# tree branch, select Test, and in the list of templates click Test Project. Name the project FooTheory.GetContactCmdLet.Test, as is shown below:

- Click OK to create the test project.
- From the generated project files, delete the manual test (ManualTest1.mht), and the generated unit test (UnitTest1.cs).
- Right-click the project and select Add | Unit Test.
- In the Create Unit Test dialog, expand the tree until you get to the cmdlet, and select all of the members below the cmdlet (which are our three main methods: BeginProcessing, ProcessRecord and EndProcessing), as show below:

Click OK to create the unit test file. Visual Studio will create a skeleton unit test with a test method for each of your selected methods. By default, the results of our tests will be set to "inconclusive" by default.
- To start out with, I cleaned up the generated the test code a bit, and created one test method which calls into all methods in sequence. The full code for the initial test is shown below:
1 // The following code was generated by Microsoft Visual Studio 2005.
2 // The test owner should check each test for validity.
3 using Microsoft.VisualStudio.TestTools.UnitTesting;
4 using System;
5 using System.Text;
6 using System.Collections.Generic;
7 using FooTheory.GetContactCmdlet;
8
9 using CmdLet = FooTheory.GetContactCmdlet.GetContactCmdlet;
10
11 namespace FooTheory.GetContactCmdLet.Test
12 {
13 /// <summary>
14 ///This is a test class for FooTheory.GetContactCmdlet.GetContactCmdlet and is intended
15 ///to contain all FooTheory.GetContactCmdlet.GetContactCmdlet Unit Tests
16 ///</summary>
17 [TestClass()]
18 public class GetContactCmdletTest
19 {
20 private TestContext testContextInstance;
21
22 /// <summary>
23 ///Gets or sets the test context which provides
24 ///information about and functionality for the current test run.
25 ///</summary>
26 public TestContext TestContext
27 {
28 get
29 {
30 return testContextInstance;
31 }
32 set
33 {
34 testContextInstance = value;
35 }
36 }
37
38 [DeploymentItem("FooTheory.GetContactCmdlet.dll")]
39 [TestMethod()]
40 public void CompleteTest()
41 {
42 CmdLet target = new CmdLet();
43
44 FooTheory.GetContactCmdLet.Test.FooTheory_GetContactCmdlet_GetContactCmdletAccessor accessor =
45 new FooTheory.GetContactCmdLet.Test.FooTheory_GetContactCmdlet_GetContactCmdletAccessor(target);
46
47 accessor.BeginProcessing();
48 accessor.ProcessRecord();
49 accessor.EndProcessing();
50
51 } // method CompleteTest
52
53 } // class GetContactCmdletTest
54 }
Do a full debug run on the test project to ensure that everything is working fine (it should be, because we are not doing anything yet.. ;-). So, now we are all set, we have our skeleton cmdlet code, we have a unit test, we are ready to start implementing this puppy!
Finally, some "real" Coding: Creating the initial version of our Cmdlet
First, we need a add a reference to the Outlook 2007 PIA to our cmdlet project. Add a reference to the Microsoft Outlook 12.0 Object Library, and is shown below:

Click OK to add the reference. Include a using statement for Microsoft.Office.Interop.Outlook and add an an explicit using statement to the cmdlet implementation class source file, as shown below:
using Microsoft.Office.Interop.Outlook;
using Outlook = Microsoft.Office.Interop.Outlook;
Now, we can create an instance of the main Outlook Application class in our SetupOutlookConnection() method, and release it in the TeardownOutlookConnection() method. Our implementation class code so far is shown below:
1 using System;
2 using System.Collections.Generic;
3 using System.Runtime.InteropServices;
4 using Outlook = Microsoft.Office.Interop.Outlook;
5
6 namespace FooTheory.GetContactCmdlet.Implementation
7 {
8 /// <summary>
9 /// This class is our implementation class for the Get-Contact
10 /// cmdlet.
11 /// </summary>
12 internal class GetContactsImpl
13 {
14 #region private fields
15 // This is our Outlook application class
16 private Outlook.ApplicationClass m_Application;
17 #endregion private fields
18
19 internal void SetupOutlookConnection()
20 {
21 // Create an instance of our Application class
22 m_Application =
23 new Microsoft.Office.Interop.Outlook.ApplicationClass();
24 }
25
26 internal void GetContacts()
27 {
28 }
29
30 internal void TeardownOutlookConnection()
31 {
32 // Release our COM object
33 Marshal.ReleaseComObject(m_Application);
34 }
35
36 } // class GetContactsImpl
37
38 } // namespace FooTheory.GetContactCmdlet.Implementation
Note have we use Marshal.ReleaseComObject to release the m_application COM object instance, since we are dealing with COM interop here, because we are invoking the methods in the PIA through a RCW (Runtime Callable Wrapper).
At this point, it would be a good idea to run the unit test again to make sure everything is running OK.
Next, we will implement a "bare minimum" version of the GetContacts method. The code is shown below:
1 /// <summary>
2 /// This method returns our list of Contacts
3 /// </summary>
4 /// <returns></returns>
5 internal List<ContactItem> GetContacts()
6 {
7 // Get the contacts folders
8 MAPIFolder contactsFolder =
9 m_Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderContacts);
10
11 // Create our output list of ContactItems and add the ContactItems
12 List<ContactItem> contacts = new List<ContactItem>();
13 foreach(ContactItem contactItem in contactsFolder.Items)
14 {
15 contacts.Add(contactItem);
16 }
17
18 return contacts;
19 }
In lines 8 and 9 we are getting a reference to the Contacts Folder, and then (line 13-15) we loop through each ContactItem and add it to our strongly-typed List. Again, at this point I would recommend a quick unit test to make sure that your code is running fine.
Now, we are ready to change the main body of our cmdlet to output the list of ContactItems. To write a single object to the pipeline, the PowerShell SDK provides a System.Management.Automation.Cmdlet.WriteObject method. We use this method to write the List<ContactItem> instance to the pipeline. The first argument to the method is our List<ContactItem> object, and the second parameter is set to true to indicate the our object is a collection, and that PowerShell should enumerate over the collection and send each object in turn through the pipeline.
We need to make sure that we add the following using statement to our main cmdlet code file:
using Microsoft.Office.Interop.Outlook;
The implementation our our ProcessRecord() method is now very simple:
1 /// <summary>
2 /// This is our main processing method
3 /// </summary>
4 protected override void ProcessRecord()
5 {
6 List<ContactItem> contacts = m_implementation.GetContacts();
7 WriteObject(contacts, true);
8 }
9
So, we simply call into our implementation to get the list of ContactItem's, and the we use WriteObject() to write this instance to the pipeline. The PowerShell runtime will enumerate through each member of our collection, and stream each object through the command pipline in turn.
One important thing that I noticed when I unit tested this first, is that I would get a NotImplementedException on the WriteObject statement, while I would NOT get this error during testing in the PowerShell console. This is probably because the unit test environment is not a "real" cmdlet host environment. Therefore, I surrounded the WriteObject with a try/catch block during my testing period as follows:
protected override void ProcessRecord()
{
List<ContactItem> contacts = m_implementation.GetContacts();
//
// We get a NotImplemented exception during testing,
// hence the try/catch. I recommend that you remove the
// try/catch after testing is complete!
//
try
{
WriteObject(contacts, true);
}
catch (NotImplementedException)
{
}
}
This should complete our initial, simple version of the cmdlet. Time to bring out the PowerShell console to do some "real" testing!
Testing our initial Cmdlet version in the PowerShell Console
Installing the Cmdlet
Before we can test our cmdlet, we first need to install the assembly which contains our cmdlet, and use the Add-PSSnapIn cmdlet to add the snap-in to our PowerShell console. And of course, since we are using PowerShell here, what other tools should we use to do this task? In the Installation sub-folder I have two scripts:
- InstallCmdlet.ps1. This script installs our assembly, and adds the snap-in to the console.
- UninstallCmdlet.ps1: This script removes the snap-in and performs an uninstall of our assembly.
The contents of InstallCmdlet.ps1 is shown below:
installutil ..\bin\debug\FooTheory.GetContactCmdlet.dll
Add-PSSnapIn FooTheory.GetContactCmdlet
To register the cmdlet, follow these steps:
- Open a PowerShell prompt, and navigate to the Installation sub-directory of your project.
- Run the InstallcmdLet.ps1 script, as is shown below:

Our cmdlet is now registered and ready to go. Note that you will have to run the registration script each time you start up a new PowerShell console, and you want to use the Get-Contact cmdlet. You will notice that you get tired of this real quick, so I recommend that you run the installation script from your user profile script, which is typically located at:
C:\Users\<UserName>\Documents\WindowsPowerShell\profile.ps1
(Substitute <UserName> with your user name). For example, this is the code I added to the end of my profile.ps1 file:
# Register our Get-Contact cmdlet
Push-Location "c:\bhaelen\Posts\Custom cmdlets for PowerShell\Code\FooTheory.GetContactCmdlet\Installation"
.\InstallCmdlet.ps1 > $null
Pop-Location
Notice also how I redirect the output of the install to $null, so we don't get the full output of the install and registration command each time we startup the PowerShell console.
Before we can start testing, we should first make sure that our Outlook 2007 instance is configured correctly.
Configuring Outlook
You can configure Outlook to allow or disallow programmatic access by selecting Tools | Marco | Security. In the Trust Center dialog, select Programmatic Access, as is shown in the screen shot below:

The default option is the second option (always warn me about suspicious activity). You can leave Outlook at this setting, but then you will get a prompt each time you run the Get-Contact cmdlet. The recommended setting is the first option (Warn me about suspicious activity when my antivirus software is inactive or out of date) so I recommend that you use this setting while you are testing the Get-Contact cmdlet.
Testing our cmdlet
To find out what kind of information you can get out of our Get-Contact cmdlet, run the following command in the PowerShell console:
Get-Contact | Get-Member -MemberType property
When you run this command, you will notice that you get a (very) long list of properties, a very small sub-set of which is shown below:
TypeName: System.__ComObject#{00063021-0000-0000-c000-000000000046}
Name MemberType Definition
---- ---------- ----------
Account Property string Account () {get} {set}
Actions Property Actions Actions () {get}
Anniversary Property Date Anniversary () {get} {set}
Application Property _Application Application () {get}
AssistantName Property string AssistantName () {get} {set}
AssistantTelephoneNumber Property string AssistantTelephoneNumber () {get} {set}
Attachments Property Attachments Attachments () {get}
AutoResolvedWinner Property bool AutoResolvedWinner () {get}
BillingInformation Property string BillingInformation () {get} {set}
Birthday Property Date Birthday () {get} {set}
Body Property string Body () {get} {set}
Business2TelephoneNumber Property string Business2TelephoneNumber () {get} {set}
BusinessAddress Property string BusinessAddress () {get} {set}
BusinessAddressCity Property string BusinessAddressCity () {get} {set}
BusinessAddressCountry Property string BusinessAddressCountry () {get} {set}
BusinessAddressPostalCode Property string BusinessAddressPostalCode () {get} {set}
BusinessAddressPostOfficeBox Property string BusinessAddressPostOfficeBox () {get} {set}
BusinessAddressState Property string BusinessAddressState () {get} {set}
BusinessAddressStreet Property string BusinessAddressStreet () {get} {set}
BusinessCardLayoutXml Property string BusinessCardLayoutXml () {get} {set}
BusinessCardType Property OlBusinessCardType BusinessCardType () {get}
BusinessFaxNumber Property string BusinessFaxNumber () {get} {set}
BusinessHomePage Property string BusinessHomePage () {get} {set}
BusinessTelephoneNumber Property string BusinessTelephoneNumber () {get} {set}
CallbackTelephoneNumber Property string CallbackTelephoneNumber () {get} {set}
CarTelephoneNumber Property string CarTelephoneNumber () {get} {set}
Categories Property string Categories () {get} {set}
Children Property string Children () {get} {set}
Class Property OlObjectClass Class () {get}
Companies Property string Companies () {get} {set}
CompanyAndFullName Property string CompanyAndFullName () {get}
CompanyLastFirstNoSpace Property string CompanyLastFirstNoSpace () {get}
CompanyLastFirstSpaceOnly Property string CompanyLastFirstSpaceOnly () {get}
CompanyMainTelephoneNumber Property string CompanyMainTelephoneNumber () {get} {set}
CompanyName Property string CompanyName () {get} {set}
ComputerNetworkName Property string ComputerNetworkName () {get} {set}
Conflicts Property Conflicts Conflicts () {get}
ConversationIndex Property string ConversationIndex () {get}
ConversationTopic Property string ConversationTopic () {get}
CreationTime Property Date CreationTime () {get}
CustomerID Property string CustomerID () {get} {set}
For example, if you want to retrieve the full name and the first email address of your contacts, you can run the following command in the console:
Get-Contact | Format-Table -Property FullName, Email1Address -Autosize
This produces a nicely formatted table which looks something like this:
FullName Email1Address
-------- -------------
Matthew Ortiz /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=mortiz
Kyle Sanford ksanford@statera.com
Noah Dipasquale /o=STATORG/ou=FIRST ADMINISTRATIVE GROUP/cn=RECIPIENTS/cn=NDIPASQUALE
Wayne Macdonald /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=wmacdonald
Bob Mills Bob_Mills@isagenix.net
Jeremy Campbell /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=jcampbell
Pete Miller /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=pmiller
Aendenne Armour /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=aarmour
Jon Robinson jRobinson@statera.com
Erl Egestad /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=eegestad
Julie Clint /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=jclint
Eric Boocock /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=eboocock
Sayward Flint /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=sflint
Malcolm Boswell /o=STATORG/ou=FIRST ADMINISTRATIVE GROUP/cn=RECIPIENTS/cn=MBOSWELL
Mike Citro /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=mcitro
Carol Jenner /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=cjenner
Ray Kwan /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=rkwan
Brian Hensel /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=bhensel
Chris Banker /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=cbanker
Chad Kingsley /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=ckingsley
Tony Blodgett /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=tblodgett
Donn Felker /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=dfelker
So, our custom cmdlet is working nicely, we can integrate it in the pipeline, it nicely cleans up after itself, so now we can go ahead and implement our more advanced features, starting with supporting arguments and parameters.
Adding Support for Arguments and Parameters
PowerShell command structure
In PowerShell, a basic command looks as follows:
command -parameter1 -parameter2 argument1, argument2
Where:
- Command: This is the name of the command. A command can be one of the following:
- A cmdlet (like our Get-Contact)
- A shell function
- A script command
- A native Windows command (for example: Notepad, Calc, etc.)
A command can be followed by zero or more parameters.
- parameter1. This is a parameter that takes no arguments, often called a switch parameter.
- parameter2. This is a parameter that takes two arguments
- argument1. First argument to parameter2
- argument2. Second argument to parameter2
Adding Parameter support to Get-Contact
As we mentioned in our requirements, we would like to enable our cmdlet to filter the returned list of contacts by the Last Name of the Contact. The user should be able to specify one or more names, and furthermore, should be able to use wildcards and regular expressions to specify the last name he/she is looking for (for example: give me all contacts which have a last name starting with A, B or C.).
To support this functionality, we will add a LastName property to our GetContactCmdlet class. The full declaration of this property is shown below:
1 /// <summary>
2 /// This is our "LastName" parameter. This is a positional
3 /// parameter, which accepts an array of values, and supports
4 /// wildcard expansion. This parameter can also accept input
5 /// from the pipeline, and has a "ln" shorthand alias
6 /// </summary>
7 [Parameter(
8 Position = 0,
9 Mandatory = false,
10 ValueFromPipeline=true,
11 ValueFromPipelineByPropertyName = true,
12 HelpMessage = "This are the Last Names of the Contact. You can use wildcards")]
13 [ValidateNotNullOrEmpty]
14 [Alias("ln")]
15 public string[] LastName
16 {
17 get { return m_lastNames; }
18 set { m_lastNames = value; }
19 }
20
The type of our LastName property is string[], because we want to allow the user to specify a list of last of last names, hence the declaration as an array. We backed this property with the m_lastNames private field. We adorned our property with a System.Management.Automation.ParameterAttribute, which identifies our property as a parameter of the Get-Contact cmdlet. The name of the parameter is the name of our property ("LastName").
In our cmdlet class, we will pass the m_lastNames array to the GetContacts() method of our implementation class, as is shown below:
1 /// <summary>
2 /// This is our main processing method, called
3 /// once for each input record in the pipeline
4 /// </summary>
5 protected override void ProcessRecord()
6 {
7 // Invoke our implementation method, passing in our array of last
8 // names
9 List<ContactItem> contacts = m_implementation.GetContacts(m_lastNames);
10
11 //
12 // We get a NotImplemented exception during testing,
13 // hence the try/catch. I recommend that you remove the
14 // try/catch after testing is complete!
15 //
16 try
17 {
18 WriteObject(contacts, true);
19 }
20 catch (NotImplementedException)
21 {
22 }
23
24 } // method ProcessRecord
Our ParameterAttribute contains the following members:
- Postion. This member identifies the LastName parameter as a positional parameter with position 0. This means that the first argument the user enters on the command line will be automatically inserted for the parameter. This implies that the user can omit the name of the parameter itself, so the following commands will be semantically identical:
Get-Command -LastName Hensel, Banker
Get-Contact Hensel, Banker
- Mandatory. This member indicates whether or not the parameters is required when invoking the cmdlet. Clearly, in our case the parameter is optional.
- ValueFromPipeline. This member indicates whether our cmdlet parameter can take values from incoming pipeline objects. For example, if you have a file Names.txt, containing a list of LastNames, you could use the content of this file as the left-hand side of a pipeline as follows:
14# Get-Content Names.txt | Get-Contact | Format-Table FullName, Email1Address -AutoSize
FullName Email1Address
-------- -------------
Chris Banker /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=cbanker
Brian Hensel /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=bhensel
Aendenne Armour /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=aarmour
The content of the Names.txt file is shown below:
17# Get-Content Names.txt
Banker
Hensel
Armour
Being able to process input from the pipeline opens up all kinds of possibilities for our cmdlet, since it now will be able to fully participate in the PowerShell pipeline processing mechanisms. I would definitely recommend always supporting this feature, since it does require no parsing work except for the adding the ValueFromPipeline member to the [Parameter] declaration!
- ValueFromPipelineByPropertyName. If this property is set to true, the Windows PowerShell runtime will check incoming pipeline properties for a LastName property. If the incoming object has such a property, the runtime will bind the FullName parameter to the FullName property of the incoming object.
The sample code contains a class library assembly called FooTheory.SampleInputPipelineLib. This class library contains a simple class called Customer, which has a LastName property, and has a simple constructor that takes a lastName argument:
1 public class Customer
2 {
3 private string m_lastName;
4
5 public string LastName
6 {
7 get { return m_lastName; }
8 set { m_lastName = value; }
9 }
10
11 public Customer(string lastName)
12 {
13 m_lastName = lastName;
14 }
15 }
16
In the following PowerShell script we load this assembly, create an array of two customer objects, and use this array as the left-hand argument of a Get-Contact pipeline:
1 [System.Reflection.Assembly]::LoadFile($(Resolve-Path .).Path + "\FooTheory.SampleInputPipelineLib.dll") > $null
2 $myCustomer = @($(New-Object FooTheory.SampleInputPipelineLib.Customer("Hensel")),
3 $(New-Object FooTheory.SampleInputPipelineLib.Customer("Banker")))
4 $myCustomer | Get-Contact | Format-Table -Property FirstName, LastName, CompanyName -Autosize
As you can see, this script produces the correct output:
79# .\SampleObjectTest.ps1
FirstName LastName CompanyName
--------- -------- -----------
Brian Hensel Statêra
Chris Banker Statêra
- HelpMessage: This argument provides a short description for this parameter.
In addition to the [Parameter] attribute, we also specify a attribute of type System.Management.Automation.ValidateNotNullOrEmptyAttribute. This attribute validates that the argument of an optional parameter is not null, an empty string or an empty collection.
The last attribute is of type System.Management.Automation.AliasAttribute. This attribute allows us to define an alias for the parameter, in this case we specified "ln" as an alias for "LastName", as is shown below:
5# get-contact -ln Banker | ft FullName, Email1Address -auto
FullName Email1Address
-------- -------------
Chris Banker /o=StatOrg/ou=First Administrative Group/cn=Recipients/cn=cbanker
Adding the attributes as specified above is the only thing we need to do to get all this functionality! The only feature which requires just a little bit of work is support for wildcard arguments, which is a topic we will tackle next.
Important note: Whenever you are re-building your cmdlet, make sure that you exit all of your PowerShell console instances. Otherwise, you will not be able to successfully build your cmdlet, since it will be locked by the PowerShell instance(s).
Adding Support for Wildcard Arguments
To support wildcards and wilcard expansion, the PowerShell SDK offers the System.Management.Automation.WildcardPattern class. The matching process itself is controlled by the System.Management.Automation.WildcardOptions flags enumeration. The enumeration supports the following options:
- Compiled. When this option is selected, the wildcard pattern is compiled into an assembly, which yields faster execution, but will increase startup time a bit.
- IgnoreCase. When this option is selected, the matching is performed in a case-insensitive manner.
- None. When this option is selected, no special processing is performed.
In our implementation, we will select a combination of the Compiled and IgnoreCase flags, so we will do a case-insensitive matching. To create an instance of the WilcardPattern class, you pass in the pattern, and the selected options, as is shown below:
// Loop over all names
foreach (string lastName in lastNames)
{
// Perform the Wildcard expansion, and add the ContactItem
// if we have a match
WildcardPattern wildcard = new WildcardPattern(lastName, options);
.......
}
Here, we are looping over each last name in our array, and we create a WildcardPattern for each last name (which might contain a wildcard pattern).
The WildcardPattern class supports a IsMatch() method, which returns true if a match is found:
if (contactItem.LastName != null && wildcard.IsMatch(contactItem.LastName))
{
contacts.Add(contactItem);
break;
}
Here, we pass in the LastName of the current contact into the IsMatch() method, and add the contact to our list of results when a match is found.
Our full implementation of the GetContacts() method is shown below:
1 /// <summary>
2 /// This method returns our list of Contacts
3 /// </summary>
4 /// <returns></returns>
5 internal List<ContactItem> GetContacts(string[] lastNames)
6 {
7 // Create our output list of ContactItems
8 List<ContactItem> contacts = new List<ContactItem>();
9
10 // Get the contacts folders from the current Session
11 MAPIFolder contactsFolder =
12 m_Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderContacts);
13
14 // Set our wildcard options
15 WildcardOptions options = WildcardOptions.IgnoreCase |
16 WildcardOptions.Compiled;
17
18 // Enumerate the Contact Items
19 foreach (ContactItem contactItem in contactsFolder.Items)
20 {
21 // Did we want to filter on LastName?
22 if (lastNames != null)
23 {
24 // Loop over all names
25 foreach (string lastName in lastNames)
26 {
27 // Perform the Wildcard expansion, and add the ContactItem
28 // if we have a match
29 WildcardPattern wildcard = new WildcardPattern(lastName, options);
30 if (contactItem.LastName != null && wildcard.IsMatch(contactItem.LastName))
31 {
32 contacts.Add(contactItem);
33 break;
34 }
35 }
36 }
37 else
38 {
39 // No filter, simply add the Contact Item
40 contacts.Add(contactItem);
41 }
42 } // foreach
43
44 return contacts;
45
46 } // method GetContacts
As you see, our implementation is rather simple, we check if a filter (lastNames array) was specified, if not, we add every contact, otherwise we just add those contacts that match our wildcard patterns.
Testing our Cmdlet
Below are a number of examples that test the wildcard processing of our cmdlet:
11# Get-Contact [a-c]* | Format-Table -Property LastName, FirstName, JobTitle
LastName FirstName JobTitle
-------- --------- --------
Campbell Jeremy VP of Strategic Resources
Armour Aendenne Consultant
Clint Julie Office Manager
Boocock Eric CRM Practice Director
Boswell Malcolm Client Partner
Citro Mike Consultant
Banker Chris Consultant
Blodgett Tony Consultant
12# Get-Contact [a-c]*, *er | Format-Table -Property LastName, FirstName, JobTitle
LastName FirstName JobTitle
-------- --------- --------
Campbell Jeremy VP of Strategic Resources
Miller Pete VP of Services
Armour Aendenne Consultant
Clint Julie Office Manager
Boocock Eric CRM Practice Director
Boswell Malcolm Client Partner
Citro Mike Consultant
Jenner Carol Consultant
Banker Chris Consultant
Blodgett Tony Consultant
Felker Donn Consultant
13# Get-Contact *e?? | Format-Table -Property LastName, FirstName, JobTitle
LastName FirstName JobTitle
-------- --------- --------
Campbell Jeremy VP of Strategic Resources
Boswell Malcolm Client Partner
Blodgett Tony Consultant
Summary
This concludes the development of our cmdlet. As you can see, the Windows PowerShell scripting language is a very powerful, productive and highly customizable development environment. Developing custom cmdlets is very easy, since all of the hard work like parameter parsing, passing objects to the command line, wildcard expansion etc. is performed by the PowerShell runtime.
I hope you have enjoyed this article, and I encourage you to experiment with this environment, and I guarantee you that you will never go back to the "good old" Windows command prompt! And of course, feedback is always appreciated!
Technorati Tags: PowerShell - Outlook 2007 - COM Interop