Sunday, May 07, 2006 1:00 PM bart

Windows PowerShell - Make your application manageable - Write your first cmdlet

Introduction

I hope lots of you have been investigating Windows PowerShell (formerly known as Monad or MSH) already and have been downloading RC1 lately. In the past I've been blogging about Monad (my apologies for still using the codename, I'm hearing myself talking about things such a Whidbey, Yukon, Longhorn and Office 12 quite a bit as well) from the end-user's (that's the IT pro's) perspective. In today's post, I'm going to focus on the developer's side of the story.

PowerShell programmability

Programmability of Windows PowerShell is an absolute must. In the end, the idea of Monad is to close the gap between GUI administration tools and their much beloved command-line brothers that play a central role in IT admins' nightly dreams :-). And of course those IT folks are right: in order to be productive, things need to be scriptable and have a big need for automation. Historically, there have been many technologies that aided in doing those kind of jobs, including ADSI, WMI and WSH but none of those were delivering the complete story. And you, as a developer, are responsible to make your application manageable by leveraging the power of those (boring?) technologies. That's where things such as WMI events, System.Management and other API's such as EIF (Enterprise Instrumentation Framework) enter the scene.

Monad's primary role is to deliver a great shell that provides access to virtually anything (don't forget the father of WMI is also Monad's father, i.e. Jeffrey Snover) by means of providers and cmdlets. By building cmdlets that capture some kind of atomic tasks (e.g. create a mailbox, stop a process, delete an item, etc) and taking advantage of an object-oriented pipeline, Monad gives IT folks a lot of flexibility compared to the things that exist today (toolboxes full of .exe files spitting out nasty-to-parse text and with the lack of proper fault - not to mention the OO 'exception' word yet - handling).

So, what are providers and what are those cmdlets? An overview:

  • Providers are the abstraction over hierarchical data stores such as a file system. Monad ships with a couple of those (of which the instances are called "PS drives", see get-psdrive) to mount your file drives but also the registry, the certificate store, etc in the shell. So, you can do things like:

    PS C:\> cd HKLM:\SOFTWARE\Microsoft
    PS HKLM:\SOFTWARE\Microsoft> dir
  • Cmdlets are little pieces of functionality that's callable using the PS shell, just like normal commands in a classic shell. The main difference is that those cmdlets are 100 percent managed code and don't live inside executable files. Furthermore, cmdlets work with objects instead of accepting text as input and spitting out text as output. For example, performing a dir operation (which is an alias for get-childitem) results in System.IO.FileInfo instances if you're calling it in the context of a "file drive". The idea is that standalone cmdlets perform a little piece of work and become great automation mechanisms when working together with other cmdlets in a pipeline structure:

    get-process | where { _$.ProcessName -eq "notepad" } | stop-process

Cmdlets form the groundwork for manageability in the future. The idea is pretty simple: encapsulate your manageability "tasks" in cmdlets that might just be little fa├žades over more complex structures inside your application (which can consist of portions of unmanaged code - cf. Exchange 12 manageability through (managed code) cmdlets). GUI-based administration tools then call into those cmdlets by hosting the Monad engine that has the full pipelining support in it, the extended type system and much more to aid in parsing, error handling, etc. And of course, those GUI-based administration tools can be writting using the MMC 3.0 technology which is the graphical cornerstone of the manageability story. By layering the GUI administration tools on top of the cmdlets, one creates also the opportunity for reproducibility by means of recording scripts. In the end, all you do in the GUI tools can be done using cmdlets as well. The cmdlets play the role of the interface to the rest of the system if you want to put it that way. And so, running GUI tools in a kind of script recording mode (somewhat equivalent to the SQL Management Studio's capabilities to record SQL scripts while working inside the GUI admin tools) is the best script generator ever seen (you'll be able to create scripts just like you record Word macros).

As a developer, you'll take care of creating those cmdlets (and maybe providers as well) to help your IT brothers to do a better job by playing with an application you can now call manageable (okay, you shouldn't forget about performance counters and event logging as well :-)). In this post, we'll take a look at the creation process of cmdlets.

Your first cmdlet

Let's take a look at the steps you have to go through in creating your first cmdlet. In order to show the richness of the processing pipeline etc we can't afford to play around with a "hello world" like sample, so we'll have to look a little further. Instead, we'll build a cmdlet (btw, that's pronounced as "command-let") that looks for regular expression matches in a (set of) given file(s).

Step 1 - Create a Visual Studio project

Cmdlets are .NET classes as I mentioned before. So to kick off, we'll create a new class library project (e.g. in C#) and give it a meaningful name, for example FindString. Next, add a reference to the System.Management.Automation.dll assembly which you can find in %programfiles%\Windows PowerShell\v1.0.

Step 2 - Declare the cmdlet class

Next, it's time to declare our cmdlet-acting class as follows:

using System.Management.Automation;

[
Cmdlet("Get", "Matches"
)]
public class FindString :
PSCmdlet
{
}

Note: Using the CmdletAttribute (you know you can omit the -Attribute suffix in C#) you indicate the verb and noun of the cmdlet. Every cmdlet in Monad follows this pattern for clarity. For the verbs, it's recommended to take one from the list in the Windows SDK. The comment in the get-row over there mentions, so let's follow this guidance:

Get

This verb retrieves data. Use with Set.

Do not use verbs such as read, open, cat, type, dir, obtain, dump, acquire, examine, find, or search.

The class has to inherit from PSCmdlet and has to be annotated with the CmdletAttribute attribute as you can see.

Step 3 - Parameterize the cmdlet

Now it's time to take a look at the functionality itself. This consists of two steps: add parameters to the cmdlet and write the real functionality. We'll start by defining the parameters by declaring a couple of public properties and decorating those with the ParameterAttribute attribute and a couple of named parameters, as shown below (defined using the prop codesnippet in C#):

private string pattern;

[
Parameter(Mandatory=true
, Position=0)]
public string
Pattern
{
   get { return
pattern; }
   set { pattern = value
; }
}

private string
[] fullName;

[
Parameter(Mandatory=true, Position=1, ValueFromPipelineByPropertyName=true
)]
public string
[] FullName
{
   get { return
fullName; }
   set { fullName = value
; }
}

The first parameter is called Pattern and will contain the regular expression pattern we'll look for. It's a mandatory parameter and has to appear in the first position of the cmdlet's syntax (you'll see it's possible to specify those parameters in a bunch of different ways). The second one is called FullName and is also mandatory but appears in the second (and following) position(s). It's of type string[] so that it can contain more than one filename, and it can be fed in through the pipeline by the property name. That means that cmdlets that return objects with a public property called FullName can be linked to this cmdlet by means of piping (|).

Take a look at the following:

PS C:\> $a = get-childitem *.txt
PS C:\> $a | get-member F*


   TypeName: System.IO.FileInfo

Name     MemberType Definition
----     ---------- ----------
FullName Property   System.String FullName {get;}

Get-childitem (abbreviated as dir or ls) returns instances of type System.IO.FileInfo which contains a property called FullName. That enables us to pipe this data in and use it in our cmdlet as a parameter. So, you'll be able to write things like:

dir | get-match [a-z]*

Step 4 - Implement the core functionality

The core of the cmdlet's implementation goes in the ProcessRecord method (note: asynchronous variants of this method also exist, we're not going to focus on those for now). The following is a pretty short implementation of our matching logic relying on System.Text.RegularExpressions and System.IO to read files:

protected override void ProcessRecord()
{
   List<Match> matches = new List<Match>();

   ProviderInfo
info;
   foreach (string item in FullName)
      foreach (string file in GetResolvedProviderPathFromPSPath(item, out info))
         using (StreamReader sr = File.OpenText(file))
            matches.Add(
new Match(file, Regex.Matches(sr.ReadToEnd(), pattern)));

   WriteObject(matches);
}

The call to WriteObject returns the result to the pipeline for further processing; other cmdlets could use our output as their input, or the user can just feed this in to cmdlets like format-list (or work with variables, etc). To aid our implementation, we've created a simple Match class:

public class Match
{
  
public Match(string file, MatchCollection
matches)
   {
     
this
.file = file;
     
this
.matches = matches;
   }

  
private MatchCollection
matches;

  
public MatchCollection
Matches
   {
     
get { return
matches; }
     
set { matches = value
; }
   }

   private string
file;

   public string
File
   {
     
get { return
file; }
     
set { file = value
; }
   } 
}

Step 5 - Create a custom shell

Time to compile the cmdlet and create a custom shell. To do this, follow each of those:

  1. Compile the cmdlet and copy the FindString.dll file to the Windows PowerShell\v1.0 installation folder (for ease of testing right now).
  2. Open Windows PowerShell and execute the following commands to build a new shell using the Make Kit:

    PS C:\Program Files\Windows PowerShell\v1.0> make-shell -out test -ns Demo -ref findstring.dll
    Windows(R) PowerShell MakeKit
    Copyright (C) 2005 Microsoft Corporation. All rights reserved.

    Shell test.exe is created successfully.

  3. If you try to start the shell now, you'll get the following error because the shell is not registered yet:

    PS C:\Program Files\Windows PowerShell\v1.0> test
    Error loading the extended type data file:
    Cannot find the registry key: "SOFTWARE\Microsoft\PowerShell\1\ShellIds\Demo.test\Path", using "C:\Program Files\Windows PowerShell\v1.0" to load the configuration files.

    There were errors in loading the format data file:
    Cannot find the registry key: "SOFTWARE\Microsoft\PowerShell\1\ShellIds\Demo.test\Path", using "C:\Program Files\Windows PowerShell\v1.0" to load the configuration files.


    To solve this issue, register the shell as follows:

    PS C:\Program Files\Windows PowerShell\v1.0> cd HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds
    PS HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds> mkdir Demo.test

       Hive: Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\ShellIds

    SKC  VC Name                           Property
    ---  -- ----                           --------
      0   0 Demo.test                      {}

    PS HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds> cd Demo.test
    PS HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Demo.test> new-itemproperty -path . -name Path -value "C:\Program Files\Windows PowerShell\v1.0\test.exe"

    PSPath       : Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\She
                   llIds\Demo.test
    PSParentPath : Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\PowerShell\1\She
                   llIds
    PSChildName  : Demo.test
    PSDrive      : HKLM
    PSProvider   : Registry
    Path         : C:\Program Files\Windows PowerShell\v1.0\test.exe

Step 6 - Testing time

Launch the shell and test whether it provides access to the newly written cmdlet (it should!):

PS C:\Program Files\Windows PowerShell\v1.0> get-command -noun matches
PS C:\Program Files\Windows PowerShell\v1.0> test
PS C:\Program Files\Windows PowerShell\v1.0> get-command -noun matches

CommandType     Name                            Definition
-----------     ----                            ----------
Cmdlet          Get-Matches                     Get-Matches [-Pattern] <Stri...


PS C:\Program Files\Windows PowerShell\v1.0>

A now, the big test:

PS C:\Program Files\Windows PowerShell\v1.0> get-matches Wmi about_Alias.help.txt

Matches                                 File
-------                                 ----
{Wmi, Wmi, Wmi}                         C:\Program Files\Windows PowerShell\...

Seems to be okay, isn't it? The system tells me the file about_Alias.help.txt contains 3 times the word Wmi somewhere, but where? Let's use a variable:

PS C:\Program Files\Windows PowerShell\v1.0> $a = Get-Matches Wmi about_Alias.help.txt
PS C:\Program Files\Windows PowerShell\v1.0> $b = $a[0]
PS C:\Program Files\Windows PowerShell\v1.0> $b

Matches                                 File
-------                                 ----
{Wmi, Wmi, Wmi}                         C:\Program Files\Windows PowerShell\...

Remember Get-Matches returns a List<Match> object, so we'll have to select an item from this list to get the rest of the properties. Or to speak with variables, $a is from type List<Match> and $b contains a Match instance. Now, we can ask for the matches as follows:

PS C:\Program Files\Windows PowerShell\v1.0> $b.Matches


Groups   : {Wmi}
Success  : True
Captures : {Wmi}
Index    : 5780
Length   : 3
Value    : Wmi

Groups   : {Wmi}
Success  : True
Captures : {Wmi}
Index    : 6008
Length   : 3
Value    : Wmi

Groups   : {Wmi}
Success  : True
Captures : {Wmi}
Index    : 6241
Length   : 3
Value    : Wmi

Or, to print the indices directly, we can use the foreach construct (which is an alias for ForEach-Object):

PS C:\Program Files\Windows PowerShell\v1.0> $b.Matches | foreach { $_.Index }
5780
6008
6241

Or what about this?

PS C:\Program Files\Windows PowerShell\v1.0> dir *.txt | get-matches PowerShell | foreach { $total = 0 } { $total += $_[0].Matches.Count }
PS C:\Program Files\Windows PowerShell\v1.0> $total
450

<Quiz>

What's the difference between dir *.txt | get-matches PowerShell and get-matches PowerShell *.txt? Tip: how many List<Match> instances does each return?

</Quiz>

Learn more

In the future, I'll cover providers and more advanced cmdlets over here as well. So, stay tuned on my blog! For the Dutch-speaking readers, the next .NET Magazine (#13) will cover Windows PowerShell cmdlets, so make sure to get your hands on it! You can subscribe to get a copy of .NET Magazine on MSDN Netherlands.

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under:

Comments

# re: Windows PowerShell - Make your application manageable - Write your first cmdlet

Monday, May 08, 2006 7:52 AM by Lee

Excellent introduction, Bart.  

Two points about the post:
1) The VerbsCommon enumeration is a great place to go for the common verbs.  For example,
[Cmdlet(VerbsCommon.Get, "Match")]

2) Custom shells are no longer required in PowerShell.  You could alse investigate creating your cmdlet as a Snapin.

# Windows PowerShell - registering a cmdlet without a custom shell

Monday, June 26, 2006 7:52 PM by B# .NET Blog

Some time ago I published an example of how to create a PowerShell cmdlet on my blog, &quot;Windows PowerShell...

# Windows PowerShell 2.0 Feature Focus - Script cmdlets

Saturday, March 22, 2008 9:09 AM by B# .NET Blog

Two weeks ago I did a little tour through Europe spreading the word on a couple of our technologies including

# The Truth about SQLPS and PowerShell V2 | Sev17

Thursday, May 27, 2010 6:30 PM by The Truth about SQLPS and PowerShell V2 | Sev17

Pingback from  The Truth about SQLPS and PowerShell V2 | Sev17

# Is Make-Shell deprecated? - Programmers Goodies

Monday, October 17, 2011 1:20 PM by Is Make-Shell deprecated? - Programmers Goodies

Pingback from  Is Make-Shell deprecated? - Programmers Goodies

# The Truth about SQLPS and PowerShell V2

Thursday, November 14, 2013 9:20 AM by The Truth about SQLPS and PowerShell V2

Pingback from  The Truth about SQLPS and PowerShell V2