Write a PowerShell Cmdlet in C# using Visual Studio

 

By Rob Gravelle

 

 

PowerShell is Microsoft’s latest and greatest command line and scripting language for Windows operating systems. It runs on top of the .NET Framework using a syntax that is based on the IEEE POSIX 1003.2 standard for Unix shellshighly reminiscent of the original Unix csh and ksh shells. One of PowerShell’s greatest strengths is its cmdlet library. A cmdlet (short for “Command Lets”) behaves like a function but is actually an instance of a Microsoft .NET Framework class. This gives it very specific attributes that makes the cmdlet rather unique in the scripting world.  Being built from classes, cmdlets are fully extensible; not only can you extend built-in cmdlets, but you can write your own from scratch.  The later is the topic of today’s article. We’ll learn how to construct, deploy, register, and run our own custom cmdlet. We’ll also discover some shortcuts along the way that will make our task that much easier.

Say Hello to the Get-NewFile Cmdlet

The cmdlet that we will be building today will search a directory for files whose modified dates are equal to or greater than (>=) a given date.  The cmdlet will accept one input parameter which is the path to the folder containing a text file with the date.  I did not make this up for the sake of this tutorial; it is actually based on a real life Windows Scripting Host (Wsh)/VBScript batch process that I implemented for transferring data between two local area networks. The process would read the date of the last run from the file and fetch all of the files in the directory structure that were new since the last run. After a successful completion, it would update the date file with the current date and time.  Our cmdlet will be responsible for reading in the date and fetching the files. Of course, all this can be done without writing a cmdlet especially in version 2.0, which includes advanced functions that allow you to write cmdlet-like functions and scripts. However, the benefit of compiling code into a cmdlet is that it can be treated like other PowerShell cmdlets with respect to behaviors such as process piping and object iteration.

 

What you’ll Need

 

Let me start of by saying that this article is aimed at Windows administrators who already have a working knowledge of PowerShell.  If you’re just getting started, there are several good articles on the WindowsITPro site to choose from, including:

·           Steps for Getting Started with PowerShell

·           Introducing Windows PowerShell

 

It should come as no surprise that cmdlets are written in Microsoft programming languages, namely VB.net and C#.  We will be using C# here today. Don’t be scared by the programming aspect of cmdlet writing.  I’ll show you how to minimize the coding required to get up and running!

 

The IDE of choice for Microsoft development is Visual Studio.  As a Windows administrator, it is likely that you have not shelled out the bucks to buy it, but that’s OK. Microsoft also offers a free – albeit scaled down - version of Visual Studio called Visual Studio Express.  For C# development, you only need the Visual C# Studio 2010 Express or Visual C# 2008 Express Edition with SP1 editions.

 

While not essential, I would highly encourage you to download the Microsoft Windows SDK.  It contains everything that you could want to develop in .NET (except the Integrated Development Environment and Framework). In addition to numerous tools, there are programming samples of all kinds, tutorials, and a complete help system. For instance, here is what I was able to find on creating PowerShell cmdlets:

 

 

Note that my screenshots were taken in Windows XP.  Depending on which Windows version you are using, the exact appearance of the screens might be a little different.  Nonetheless, the options should be the same.

 

 

In the Samples folder, you’ll find a lot of different examples for all sorts of applications, including cmdlet writing using C#.  They are under Microsoft SDKs\Windows\v6.1\Samples\SysMgmt\WindowsPowerShell\CSharp:

 

 

The Windows SDK is available for several different Windows operating systems as an executable that downloads the required components or as an offline .iso image.

 

Before you Reinvent the Wheel…

Just as you wouldn’t startup a business without checking the copyright office for a preexisting company of the same name, you wouldn’t want to go through the trouble of writing your own cmdlet only to discover that there was already one that did what you wanted.

 

Here’s how to find out what’s readily available in PowerShell:

 

It may surprise you to learn that PowerShell has a cmdlet that returns available commands.  It’s called Get-Command. Without any additional parameters, it returns a list of all the Windows PowerShell cmdlets, their aliases, as well as functions:

CommandType     Name              Definition

-----------     ----              ----------

Alias           %                 ForEach-Object

Alias           ?                 Where-Object

Function        A:                Set-Location A:

Alias           ac                Add-Content

Cmdlet          Add-Computer      Add-Computer [-DomainName] <String> [-Credential...

Cmdlet          Add-Content       Add-Content [-Path] <String[]> [-Value] <Object[...

Cmdlet          Add-History       Add-History [[-InputObject] <PSObject[]>] [-Pass...

 

To get only the cmdlet listing, without the functions and aliases, use the –CommandType flag:

 

PS H:\>Get-Command -CommandType Cmdlet

 

If any names look promising, you can find out more about the cmdlet in question by piping the output to Format-List:

 

PS H:\>Get-Command -Name Add-Member | Format-List *

 

That will yield a lot more specifics:

 

HelpUri             : http://go.microsoft.com/fwlink/?LinkID=113280

DLL                 : C:\WINDOWS\assembly\GAC_MSIL\Microsoft.PowerShell.Commands.Utility\1.0.0.0__31bf3856ad364e35\Micr

                      osoft.PowerShell.Commands.Utility.dll

Verb                : Add

Noun                : Member

HelpFile            : Microsoft.PowerShell.Commands.Utility.dll-Help.xml

PSSnapIn            : Microsoft.PowerShell.Utility

ImplementingType    : Microsoft.PowerShell.Commands.AddMemberCommand

Definition          : Add-Member [-MemberType] <PSMemberTypes> [-Name] <String> [[-Value] <Object>] [[-SecondValue] <Object>] -InputObject <PSObject> [-Force] [-PassThru] [-Verbose] [-Debug] [-ErrorAction <ActionPreference>] [-WarningAction <ActionPreference>] [-ErrorVariable <String>] [-WarningVariable <String>

] [-OutVariable <String>] [-OutBuffer <Int32>]

DefaultParameterSet :

OutputType          : {}

Name                : Add-Member

CommandType         : Cmdlet

Visibility          : Public

ModuleName          : Microsoft.PowerShell.Utility

Module              :

Parameters          : {[InputObject, System.Management.Automation.ParameterMetadata], [MemberType, System.Management.Automation.ParameterMetadata], [Name, System.Management.Automation.ParameterMetadata], [Value, System.Management.Automation.ParameterMetadata]...}

ParameterSets       : {[-MemberType] <PSMemberTypes> [-Name] <String> [[-Value] <Object>] [[-SecondValue] <Object>] -InputObject <PSObject> [-Force] [-PassThru] [-Verbose] [-Debug] [-ErrorAction <ActionPreference>] [-WarningAction <ActionPreference>] [-ErrorVariable <String>] [-WarningVariable <String>] [-OutVariable <String>] [-OutBuffer <Int32>]}

 

The first item, the HelpUri, can be used to lookup the online help.  There, you’ll find a short description of what the cmdlet does.

 

Even if your search for built-in cmdlets proves to be fruitless, there are many online code repositories, such as the PowerShell Code Repository, where you’ll find many more user-submitted cmdlets.

 

Only after having exhausted these avenues [and also if the functionality is not available by combining built-in cmdlets] should you consider writing your own cmdlet.

 

 

Creating a PowerShell Cmdlet Project in Visual Studio

 

Visual Studio includes several templates will save you a lot of startup time, by adding the correct references to your project and creating a skeleton for your class.  Unfortunately, PowerShell cmdlets are not one of the templates.  Not a problem; there are plenty of templates available online

 

After you download and run the above templates, you’ll have the opportunity to select which exactly which templates you want to install.  You want them all don’t you?

 

 

After you’ve installed the new templates, you’ll see the PowerShellCmdlet template under “My Templates” in the New Project dialog:

 

That will create an empty project with the minimum classes and references to install:

Description: powershell_cmdlet_project_in_solution_explorer

 

We’ll need to add a file using the “Add New Item…” command:

Description: add_new_item_menu_command

The Difference between Cmdlets and PSCmdlets

As the image below demonstrates, the PowerShell Visual Studio templates that we installed earlier include four PowerShell templates’ the first two are project templates while the second two are for individual classes:

There are two cmdlet Project types to choose from: Cmdlet and PSCmdlet.  Let’s take a moment and explore their differences.

A Cmdlet is a .NET class that extends the Cmdlet class. This type of cmdlet does not depend on the Windows PowerShell runtime and can be called directly from a .NET application.

A PsCmdlet is a more complex cmdlet that is named after the PSCmdlet class, which it extends. This type of cmdlet does depend on the Windows PowerShell runtime, so it must execute within the PowerShell runspace. That has the effect of increasing the size of the compiled cmdlet object, and tighter coupling with the current version of the PowerShell runtime. Hence, our cmdlet could be used inside of PowerShell or independently in an application, whether or not there is a PowerShell instance running.

As a general rule, deriving from Cmdlet is the best choice except when you need full integration with PowerShell runtime, access to session state data, call scripts etc. Then, you have to derive from PSCmdlet.

 

Naming your Cmdlet

Cmdlet names follow a VERB-NOUN pattern; the verb describes the action to perform, while the noun identifies what to perform the action on. For example, Get-Process, which we’ll see in action shortly, returns a list of processes running on the system.

When writing a new cmdlet, you choose a verb from the predefined list of Windows PowerShell verbs.

You should choose a noun that is specific, but fairly short. It is common to use a singular noun prefixed with a shortened version of the target object’s name. So even when you expect to get a lot of objects back, resist the urge to pluralize it!  Our cmdlet will be called "Get-NewFile" even though it could return many file objects.

 

The Code

 

When you first open the template in the code editor, you’ll get a basic Cmdlet class that works but does nothing.  Notice the “: Cmdlet” after the class name; that tells the compiler that it derives from the Cmdlet base class:

 

using System.Management.Automation;

 

namespace PowerShellCmdlet1

{

    [Cmdlet(VerbsCommon.Get, "PowerShell_Cmdlet1")]

    public class PowerShell_Cmdlet1 : Cmdlet

    {

        protected override void ProcessRecord()

        {

            base.ProcessRecord();

        }

    }

}

 

We should start by setting the namespace for our cmdlet.  The recommended practice for .NET is to use the company name followed by the technology name and optionally the feature and design:

 

CompanyName.TechnologyName[.Feature][.Design]

 

As with any language, the purpose of using namespaces is to minimize clutter of the global namespace and avoid naming conflicts.  You’ll find that companies often use their domain as the CompanyName, starting with the top level domain name (e.g., .com), because it’s the most generic component:

 

Com.Robgravelle.PowerShell.Commands

 

Now we have to give our cmdlet a name. A good naming convention is to reflect the cmdlet name in the class name. To do this, use the form "VerbNounCommand" and replace "Verb" and "Noun" with the verb and noun used in the cmdlet name. Thus, our "Get-NewFile" Cmdlet’s class should be called something along the lines of “GetNewFileCommand”:

 

namespace Com.Robgravelle.PowerShell.Commands

{

    [Cmdlet(VerbsCommon.Get, "NewFile")]

    public class GetNewFileCommand : Cmdlet

    {

 

[See Code Listing 1 for the full source code for PowerShell Cmdlet1.cs]

    [Cmdlet(VerbsCommon.Get, "NewFile")]

    public class GetNewFileCommand : Cmdlet

 

 

Defining Parameters

 

PowerShell cmdlets can accept a wide array of input parameters, from positional, named, optional, dynamic, aliased, to parameter sets for different contexts. Suffice to say, there are enough parameter declaration rules to take up an article unto themselves!  We’re going to cover a few of the most common parameter types right now.

 

At its most basic, a parameter requires the [Parameter()] header along with attribute accessors (getters) and mutators (setters).  The following code defines an optional string parameter called If you’re familiar with Java or C, you’ll recognize the pattern

Our cmdlet will define one mandatory parameter called SourceDir.  It defines the directory which will contain the file that contains information about when the last processing run completed.  The cmdlet will use the date contained therein to determine the start date for the fetching of new files. Here is our parameter declaration:

 

   [Parameter(Position = 0)]

   [ValidateNotNullOrEmpty]

   public string SourceDir

   {

       get { return sourceDir; }

       set { sourceDir = value; }

   }

   private string          sourceDir;

 

Our parameter is positional because of the Position = 0 argument.  Otherwise, it would require a name

 

The ValidateNotNullOrEmpty attribute specifies that the parameter value cannot be null or an empty object.  Hence, PowerShell will generate an error if an empty string or array is passed to the cmdlet, as well as a null value.   
 

The Parameter() declaration can accept a few arguments of its own to assign some attributes to our parameter. These include a position number, whether or not the parameter is mandatory, its name, as well as its parameter set name, if applicable:
 
[Parameter(Position = 0, ParameterSetName = "SourceDir")]

 

You can also define an Alias list for the parameter. In the following example, two aliases are defined for the same parameter:

 

[Parameter(Position = 0, ParameterSetName = "SourceDir")]

[Alias("gfl","gfile")]

See Code Listing 7

 

 

Overriding Processing Methods

 

Cmdlets generally process its output and input objects to and from an object pipeline rather than to and from a stream of text. This makes them decidedly Object-Oriented and far more flexible than say your standard method. They are also record-oriented and generally process a single object at a time.  That’s important because it dictates a certain kind of processing structure that is event-driven rather than procedural. 

 

Cmdlets possess three important processing events that we may choose to override:

·           The BeginProcessing() event is used to perform one-time startup operations that would apply to all the objects processed by the cmdlet. The Windows PowerShell runtime calls this method once.

·           The ProcessRecord() event is where objects passed to the cmdlet are processed. The Windows PowerShell runtime calls this method for each object passed to the cmdlet.

·           The Windows PowerShell runtime calls the EndProcessing() method one time only to perform one-time post processing operations.

 

The BeginProcessing() Event Code

 

As stated above, this is a one-time startup event.  It’s where we would perform any initialization that needs to occur before we start processing out objects.  That may entail reading from a data store or performing lookups.  You don’t want to fetch the date in the ProcessRecord() event because it would execute for each and every object that our cmdlet processes! 

 

The following code instantiates a DirectoryInfo object from the SourceDir input string, obtains a reference to the file, and opens a text steam reader: 

  

 

        private DirectoryInfo   Dir;

        private DateTime        lastProccessedDateTime;

 

        protected override void BeginProcessing()

        {

            try

            {

                Dir = new DirectoryInfo(SourceDir);

                FileInfo fi = new FileInfo(Path.Combine(SourceDir, "last_processed_datetime.txt"));

                StreamReader sr = fi.OpenText();

                string contents = sr.ReadLine();

                try

                {

                    lastProccessedDateTime = DateTime.Parse(contents);

                }

 

Notice that the DirectoryInfo and DateTime objects are declared outside the event code.  This is so that the ProcessRecord() and EndProcessing() events also have access to them.

 

The ProcessRecord() Event Code

 

ProcessRecord() is called for every object that is passed in from the object input stream. In our case, we are passing in a single string, but it could be made to work for an array of directory trees to search.  In that case it would be called multiple times.

 

The matching files will be stored in a List.  The “<>” brackets immediately following the List declaration are called generics.  They tell the compiler what type of object the List will contain.  We’re going to be storing a collection of FileInfo objects.

 

But before we can filter the list down to those which were modified on or after the lastProccessedDateTime variable, we have to get the complete list of files in the directory tree, including subfolders.  To do that, we use the DirectoryInfo’s GetFiles() method.  It takes a file pattern – just like the Windows Search utility -  and an option constant to specifiy a shallow or deep search.

 

We can then use a foreach loop to iterate through the files array to whittle down our list.  Anything that fits within the date range in added to our files collection.  This would also be a good place to weed out any other types of files that we didn’t want.  I added code to not include the last_processed_datetime.txt file.

 

The void return type in the method signature tells the compiler that the ProcessRecord() method doesn’t return anything.  That might strike you as being a little strange since we need those files once the cmdlet has finished executing. To pass our List object on to the next process, we use WriteObject().  The true tells the system to enumerate the array, and send one process at a time to the pipeline.

 

        protected override void ProcessRecord()

        {  

            List<FileInfo> files = new List<FileInfo>();

          

            try

            {

                FileInfo[] FileList = Dir.GetFiles("*.*", SearchOption.TopDirectoryOnly);

                foreach (FileInfo fileInfo in FileList)

                {

                    if (  fileInfo.Name          != "last_processed_datetime.txt"

                       && fileInfo.LastWriteTime >= lastProccessedDateTime )

                    {

                        files.Add(fileInfo);

                    }

                }

            }

 

 

The EndProcessing() Event Code

 

This is the easiest part. We don’t require any clean up code, so we don’t have to include this event at all!

 

Exception Handling

How you handle runtime exceptions in a cmdlet depends on the nature and severity of the exception. At the highest level, there really are two main categories of exceptions that we’ll have to deal with: fatal and non-fatal.  Each of these types are associated with their own error method, ThrowTerminatingError() and WriteError() respectively.

ThrowTerminatingError() reports a fatal error when the cmdlet cannot continue, or when you do not want the cmdlet to continue to process records.  It’ll cause the Windows PowerShell runtime to start shutting down the pipeline. You can attach additional error record information that describes the condition that caused the error if you wish.  WriteError() sends the error to the error pipeline for processing but the cmdlet continues to process afterwards.

There is conversion required because errors are caught using a try/catch block, which is the defacto OO way.  It always traps some type of throwable class, either an Exception or a subclass.

 

 

Both the ThrowTerminatingError() and WriteError() methods take an ErrorRecord object as an argument, so the Exception has to be converted into it.  That’s actually not that difficult because the ErrorRecord accepts an Exception as the first parameter.  It’s the three additional ones that might take some thinking on your part.  Here’s a rundown of all four parameters:

 

·  exception: The exception that is associated was caught by the catch block.

·  errorId: A developer-defined string identifier of the error. This value is not localized so it must be applicable to any locale.

 

·  errorCategory: An ErrorCategory constant that describes the category of the error.  It can be tricky to decide which category fits best, but it’s only used for display purposes, so it isn’t worth loosing sleep over!

·  targetObject: The object that was being processed when the error occurred. This may be an input object or a temporary one created in the local scope.

 

The following code creates a new ErrorRecord using the caught SecurityException: 

 

    catch (SecurityException se)

    {

                      ThrowTerminatingError(new ErrorRecord(se, se.GetType().Name, ErrorCategory.SecurityError, Dir));

                  }

 

 

In this instance, the ErrorCategory is “NotSpecified” and the targetObject is null because we don’t know the source of the exeption:

 

           

    catch (FormatException fe)

                {

                    ThrowTerminatingError(new ErrorRecord(fe, fe.GetType().Name, ErrorCategory.InvalidData, sr));

                    Console.WriteLine("Unable to convert '{0}'.", contents);

                }

            }

            catch (SecurityException se)

            {

                ThrowTerminatingError(new ErrorRecord(se, se.GetType().Name, ErrorCategory.SecurityError, Dir));

            }

            catch (ArgumentException ae)

            {

                ThrowTerminatingError(new ErrorRecord(ae, ae.GetType().Name, ErrorCategory.InvalidData, Dir));

            }

            catch (PathTooLongException pe)

            {

                ThrowTerminatingError(new ErrorRecord(pe, pe.GetType().Name, ErrorCategory.InvalidData, Dir));

            }

            catch (Exception ex)

            {

                ThrowTerminatingError(new ErrorRecord(ex, "Exception", ErrorCategory.NotSpecified, null));

            }

        }

  ...

            catch (Exception ex)

            {

                WriteError(new ErrorRecord(ex, "General Exception", ErrorCategory.NotSpecified, null));

            }

In typical try/catch fashion, specific errors are dealth with first, and then a generic catch-all is placed at the bottom.  As you can see, dealing with the file system can throw a lot of different error types!

Here’s the updated code for the ProcessRecord() event. This code is less error-prone so one generic catch block should suffice:

Creating the Installer

Before we can call our cmdlet, we need to create a PowerShell snap-in. It will register all the cmdlets and Windows PowerShell providers in our C# project assembly and add it to the Windows PowerShell environment.

 

To create a PSSnapIn, you need to write a bit of code that will perform two tasks. First, it provides identification for your snap-in so it can be distinguished from other snap-ins installed on your system. Second, it provides information for properly installing the snap-in and for creating the appropriate registry entries to allow Windows PowerShell to find the assembly.

 

There are two types of Windows PowerShell snap-ins in the System.Management.Automation namespace: PSSnapIn and CustomPSSnapIn. You should use PSSnapIn when you want to register all the cmdlets and providers automatically in one assembly. CustomPSSnapIn should be used when you want to register a subset of the cmdlets and providers in one assembly or when you want to register cmdlets and providers that are in different assemblies.

 

It just so happens that we have a template for PSSnapIns (it is the most common).  To add it to our project, select “Add New Item…” from the menu as we did earlier.  This time select the PowerShellCmdlet SnapIn template:

 

 

You should then see it appear below our other project files in the Solution Explorer:

To open the file in the editor, don’t double-click it; that will attempt to open it with the C# Form Editor, which will fail and display an error.  Instead, right-click the file and select “View Code” from the popup menu, or, better still, set the code editor as default:

1.       Select “Open With…” from the popup menu.

2.       Select CSharp Editor from the editor list.

3.       Click the “Set as Default” button.

4.       Click OK to close the dialog and open the code editor.

Here’s what you’ll have to change to the file:

1.       Change the namespace to that of the Cmdlet class.

2.       Change the name of the class to GetNewFile_SnapIn1.

3.       Change the parent class to PSSnapIn.

4.       Enter a description in the Description() method.

5.       Copy the name of the class to the Name() method.

6.       Enter the vendor info.

7.       Update the CmdletConfigurationEntry() constructor as follows, where the ifrst argument is the name of the cmdlet, the second is the Type of the Cmdlet class, and the third is the help file:

new CmdletConfigurationEntry("Get-NewFile", typeof(GetNewFileCommand), "Get-NewFile.dll-Help.xml")

 

Here is the code for the GetNewFile_SnapIn1 class after you’ve performed the above steps:

 

using System.Collections.ObjectModel;

using System.ComponentModel;

using System.Management.Automation;

using System.Management.Automation.Runspaces;

 

namespace Com.Robgravelle.PowerShell.Commands

{

    [RunInstaller(true)]

    public class GetNewFile_SnapIn1 : PSSnapIn

    {

        //private Collection<CmdletConfigurationEntry> _cmdlets;

 

        /// <summary>

        /// Gets description of powershell snap-in.

        /// </summary>

        public override string Description

        {

            get { return "This is a PowerShell snap-in for the get-newfile cmdlet."; }

        }

 

        /// <summary>

        /// Gets name of power shell snap-in

        /// </summary>

        public override string Name

        {

            get { return "GetNewFile_SnapIn1"; }

        }

 

        /// <summary>

        /// Gets name of the vendor

        /// </summary>

        public override string Vendor

        {

            get { return "GravelleConsulting.com"; }

        }

        /*

        public override Collection<CmdletConfigurationEntry> Cmdlets

        {

            get

            {

                if (null == _cmdlets)

                {

                    _cmdlets = new Collection<CmdletConfigurationEntry>();

                    _cmdlets.Add(new CmdletConfigurationEntry

                      ("Get-NewFile", typeof(GetNewFileCommand), "Get-NewFile.dll-Help.xml"));

                }

                return _cmdlets;

            }

        } */

 

    }

}

 

Building and Installing our Cmdlet

To build the project, select Build => Build Solution from the main menu.  Keep your eye out for the “Build succeeded” message in the bottom-left corner of the GUI. Otherwise, you may have errors in the Error list pane.  Double clicking on an error will bring you to the relevant line in the code editor. You may have to clean up a few of these before you get a successful build.

You are now ready to register your cmdlet.

Run the Installutil.exe utility with the path to your assembly. When this utility runs, it creates some registry entries under HKLM\SOFTWARE\Microsoft\PowerShell\1\PowerShellSnapins\<snapinname> that are used by PowerShell to load the assembly and find the various configuration files. The version of the InstallUtil program to use varies depending on whether you are installing on a 32-bit or 64-bit platform.

 

To install 32-bit registry information, use: %systemroot%\Microsoft.NET\Framework\v2.0.50727\installutil.exe

To install 64-bit registry information, use: %systemroot%\Microsoft.NET\Framework64\v2.0.50727\installutil.exe

 

Here is the command that I used to register the cmdlet, along with the output produced:

DOS command:

E:\writings\Articles\IT\WindowsITPro\PowerShell\CmdLets\Project\PowerShellCmdlet1\PowerShellCmdlet1\bin>C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\installutil /i PowerShellCmdlet1.dll

 

 

 

Using the Cmdlet

 

To use our cmdlet, we need to load the snap-in into the current PowerShell session using the Add-PSSnapIn Cmdlet:

 

PS H:\> Add-PSSnapin GetNewFile_SnapIn1

 

You can verify that the cmdlet has been installed with the command “Get-PSSnapin –Registered”. It prints a listing of all registered cmdlets:

PS H:\> Get-PSSnapin -Registered

Name        : GetNewFile_SnapIn1   (that’s ours!)
PSVersion   : 2.0
Description : This is a PowerShell snap-in for the get-newfile cmdlet.

Name        : GetProcPSSnapIn01
PSVersion   : 2.0
Description : This is a PowerShell snap-in that includes the get-proc cmdlet.

Name        : SqlServerCmdletSnapin100
PSVersion   : 2.0
Description : This is a PowerShell snap-in that includes various SQL Server cmdlets.

Name        : SqlServerProviderSnapin100
PSVersion   : 2.0
Description : SQL Server Provider

 

Of course you’ll need to create the last_processed_datetime.txt file and insert a valid date, such as “2012-03-01 8:30 PM Thu, 01 March 2012 00:00:00 GMT\”.

 

Save the file to a directory that contains a number of folders and files, some of which were last modified before your last processed datetime, and some after.  Here’s how to save the date to a file with one PowerShell command:

 

Set-Date -date "2012-03-01 8:30 PM" | out-File "C:\My Documents\last_processed_datetime.txt"

 

 

You can run the cmdlet with the command Get-NewFile and you should see:

 

PS H:\> get-newfile "E:\unprotected\CIC PC Backup\writings\Articles\IT\WindowsITPro\PowerShell"

 

Mode                LastWriteTime     Length Name

----                -------------     ------ ----

-a---        2012-03-02   1:55 PM    6156064 WindowsXP-KB968930-x86-ENG.exe

-a---        2012-03-02   1:56 PM     130048 admFiles_PowerShell.msi

-a---        2012-03-02   2:21 PM    2472448 PowerShellV2_SDK_Samples.msi

-a---        2012-03-02   2:25 PM   23510720 dotnetfx.exe

-a---        2012-03-19   9:26 AM   24758792 NetFx20SP1_x86.exe

---h-        2012-03-27   3:16 PM     518656 ~WRL3399.tmp

 

Uninstalling the Cmdlet

 

If you ever tire of your cmdlet (no way!) or you’d like to replace it with an even more awesome one, the command Remove-PSSnapin GetNewFile_SnapIn1 will remove the cmdlet from the list that Windows PowerShell uses to find cmdlets.

 

Remove-PSSnapIn doesn't actually unload the assembly. To remove it, execute the same command from a DOS window as you did to install it, except with the /u flag instead of /i:

 

installutil -u PowerShellCmdlet1.dll

 

 

 

Conclusion

 

Now that you’ve gotten your first taste of cmdlet creation, I’m sure that you’ll come up with all sorts of ideas for some of your own.  I only wish that PowerShell had been around when I wrote those data transfer scripts several years ago.  It would have made things so much easier!

 

 

 

 

Rob Gravelle resides in Ottawa, Canada, and is the founder of GravelleConsulting.com. Rob has built systems for Intelligence-related organizations such as Canada Border Services, CSIS as well as for numerous commercial businesses. Email Rob at rob@robgravelle.com to receive a free estimate on your software project.

Rob is also an accomplished guitar player, and has released several CDs with various bands and as a solo artist. His former band, Ivory Knight, was rated as one Canada's top hard rock and metal groups by Brave Words magazine (issue #92).  MP3s of his covers project are available from iTunes and other digital music sites.

 

 

Code Listing 1: PowerShell Cmdlet1.cs

 

using System;

using System.Collections.Generic;

using System.IO;

using System.Management.Automation;

using System.Security;

 

namespace Com.Robgravelle.PowerShell.Commands

{

    [Cmdlet(VerbsCommon.Get, "NewFile")]

    public class GetNewFileCommand : Cmdlet

    {

        [Parameter(Position = 0)]

        [ValidateNotNullOrEmpty]

        public string SourceDir

        {

            get { return sourceDir; }

            set { sourceDir = value; }

        }

        private string          sourceDir;

        private DirectoryInfo   Dir;

        private DateTime        lastProccessedDateTime;

 

        protected override void BeginProcessing()

        {

            try

            {

                Dir = new DirectoryInfo(SourceDir);

                FileInfo fi = new FileInfo(Path.Combine(SourceDir, "last_processed_datetime.txt"));

                StreamReader sr = fi.OpenText();

                string contents = sr.ReadLine();

                try

                {

                    lastProccessedDateTime = DateTime.Parse(contents);

                }

                catch (FormatException fe)

                {

                    ThrowTerminatingError(new ErrorRecord(fe, fe.GetType().Name, ErrorCategory.InvalidData, sr));

                    Console.WriteLine("Unable to convert '{0}'.", contents);

                }

            }

            catch (SecurityException se)

            {

                ThrowTerminatingError(new ErrorRecord(se, se.GetType().Name, ErrorCategory.SecurityError, Dir));

            }

            catch (ArgumentException ae)

            {

                ThrowTerminatingError(new ErrorRecord(ae, ae.GetType().Name, ErrorCategory.InvalidData, Dir));

            }

            catch (PathTooLongException pe)

            {

                ThrowTerminatingError(new ErrorRecord(pe, pe.GetType().Name, ErrorCategory.InvalidData, Dir));

            }

            catch (Exception ex)

            {

                ThrowTerminatingError(new ErrorRecord(ex, "Exception", ErrorCategory.NotSpecified, null));

            }

        }

 

        protected override void ProcessRecord()

        {  

            List<FileInfo> files = new List<FileInfo>();

          

            try

            {

                FileInfo[] FileList = Dir.GetFiles("*.*", SearchOption.TopDirectoryOnly);

                foreach (FileInfo fileInfo in FileList)

                {

                    if (  fileInfo.Name          != "last_processed_datetime.txt"

                       && fileInfo.LastWriteTime >= lastProccessedDateTime )

                    {

                        files.Add(fileInfo);

                    }

                }

            }

            catch (Exception ex)

            {

                WriteError(new ErrorRecord(ex, "General Exception", ErrorCategory.NotSpecified, null));

            }

            // Write the files to the pipeline making them available

            // to the next Cmdlet.  The "true" tells the system to

            // enumerate the array, and send one process at a time to

            // the pipeline.

            WriteObject(files, true);

        }

    }

}