Using PowerShell, Writing and Debugging Cmdlets

The prompt is dead, long live the prompt! That's the feeling PowerShell should give you, the new command line shell from Microsoft. Today, I'll try to determine exactly what the Power part in PowerShell stands for.

What exactly makes it so powerful?

I belief it will gain its fair share of supporters from the fact that it's so extensible. To demonstrate it's flexibility, I'll be writing a small extension today, a so called Cmdlet. By the end of the post, this should be the result:

PowerShell - Layout - Cmdlet

As you can see, it looks just like a normal command prompt, on steroids however.

First of all, PowerShell has tab completion, not only for file and directory names, but also for commands and parameters. Typing 'Get-H' and hitting tab will cycle through 'Get-History', 'Get-Host' and 'Get-Help'. Typing a dash behind a command, indicating you want to add a property and hitting tab will cycle through all available properties for the command, for example typing 'Format-List -' and hitting tab will give you 'Format-List -Property' followed by all other properties.

Another thing which gives PowerShell its power is the fact that it runs on .NET, which enables you to write things like:

[code] PS C:> $arr = New-Object System.Collections.ArrayList PS C:> $arr.Add("David") PS C:> $arr.Add("Cumps") PS C:> $arr.Count 2 PS C:> $arr David Cumps [/code]

Besides typing these commands in the shell itself, you can also write them in a text file, with the extension .ps1, and execute these scripts in the shell. To execute script however, the security has to be changed first, since by default PowerShell will only run signed scripts, no matter where they come from.

To change the security to run your own unsigned scripts, but still require downloaded scripts to be signed, type the following into PowerShell:

[code] Set-ExecutionPolicy RemoteSigned [/code]

Just like known Linux shells, PowerShell always runs a script when you start it, the so called profile script. Typing $profile in PowerShell will display its location on your system. Initially no profile will exist yet, so let's set one up...

Type 'notepad $profile' in PowerShell to open a new Notepad instance. When you run Vista as a non-admin, this might give a 'file does not exist' message. You can safely ignore this, as long as you save your file to the correct location afterwards (the path that $profile displayed).

The first change will be a very simple one, changing the title and window colors. Paste the following into Notepad and save it:

[code]

Set up PowerShell looks

(Get-Host).UI.RawUI.BackgroundColor = "Black" (Get-Host).UI.RawUI.ForegroundColor = "White" (Get-Host).UI.RawUI.WindowTitle = "David Cumps » PowerShell"

And go!

cls [/code]

If you now restart PowerShell, or type '. $profile', the profile will be applied and you'll notice we have a standard black and white prompt again, with our custom title. This is the first custom script PowerShell has run on your computer. To get more scripts, have a look at The Script Center Script Repository, which is filled with scripts for various system administration purposes.

Scripts are scripts however, they simple use commands available to them. To see a list of all available commands enter 'Get-Command' and have a look, typing 'Get-Alias' will give you a list of all familiar DOS commands, mapped to their respective commands. The nice part is that we can create our own custom commands, called Cmdlets, written in any .NET language and simply plug them in to PowerShell, available to help us automate tasks even easier.

By default, Microsoft ships a collection of Cmdlets, which you saw earlier, to provide all the known DOS commands for us. With these basic Cmdlets, we can already do some nice things, for example if you wanted to get all running processes ordered by CPU time, you could simply type the following:

[code] PS C:> Get-Process | Where-Object {$_.CPU -gt 0} | Sort-Object CPU -descending

Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName


783      34    60768      62960   308   226,75   2040 explorer
404      15   112932     121448   243   163,38   5480 firefox
650      16    29720      25444   136   125,03   5052 winamp
 53       2     1868      14292    48    34,22   2624 notepad
571      48    72824      67572   340    10,44   1964 devenv

[/code]

As you noticed, Cmdlets can easily be piped to each other to create powerful constructs. But, enough of the default things, let's build something!

The Cmdlet should support the following features:

  • Called stand-alone without parameters.
  • Called stand-alone with one parameter.
  • Called stand-alone with multiple parameters.
  • Called in a pipeline receiving input parameters.
  • Output its result to the pipeline.
A Cmdlet is nothing more then a Class Library which uses System.Management.Automation, inherits from Cmdlet and applies the Cmdlet attribute. Don't forget to add a reference to C:\Program Files\Reference Assemblies\Microsoft\WindowsPowerShell\v1.0\System.Management.Automation.dll when you create the new project, to get access to the Cmdlet base class and CmdletAttribute.

I'll let the code speak for itself, with a small recap afterwards.

[csharp] using System; using System.Management.Automation;

namespace CumpsD.HelloWorld { [Cmdlet(VerbsCommon.Get, "Hello")] public class HelloCmdlet : Cmdlet { // An array is used to support the following scenario: // Get-Process | Get-Hello [Parameter(Position = 0, ValueFromPipelineByPropertyName = true, Mandatory = false, HelpMessage = "Name to greet.")] public string[] Name { get; set; }

    // Called when cmdlet is invoked.
    protected override void BeginProcessing()
    {
        // You could open a database connection here for example.
        base.BeginProcessing();
    }

    // Called when we have pipeline input.
    protected override void ProcessRecord()
    {
        // this.Name could be null in BeginProcessing (no parameters passed)
        // but could be filled when ProcessRecord gets called (parameters from pipeline)

        if (this.Name == null)
        {
            // No arguments have been supplied, neither manual nor pipeline.
            this.WriteObject("Hello World!");
        }
        else
        {
            foreach (string name in this.Name)
            {
                this.WriteObject(String.Format("Hello {0}!", name));
            }
        }
    }

    // Called after every record has been processed.
    protected override void EndProcessing()
    {
        base.EndProcessing();
    }

    // Called when the user hits CTRL+C
    protected override void StopProcessing()
    {
        base.StopProcessing();
    }
}

} [/csharp]

First of all, we define the name of our Cmdlet in the attribute, Get-Hello in this case. Then we create our only property as an array, to support multiple parameters being passed in, and specify that its allowed to retrieve its value from the pipeline. When it is called through the pipeline, it will take each object returned from the previous Cmdlet, look if there is a property called Name on there, and use that value for our property.

After this initial setup, it's time to implement the processing of the Cmdlet. The PowerShell infrastructure will always call BeginProcessing and ProcessRecord, even if you don't supply any parameters, as documented in the code above.

Simply coding up this class won't cut it however. Somehow it needs to get inserted into the PowerShell environment. For this, we create a very simple installer class, which will install all Cmdlets it can find in our assembly into PowerShell. Don't forget to add System.Configuration.Install as a reference to get access to the RunInstaller attribute.

[csharp] using System; using System.ComponentModel; using System.Management.Automation;

namespace HelloWorld { [RunInstaller(true)] public class HelloSnapIn : PSSnapIn { public override string Name { get { return "HelloSnapIn"; } }

    public override string Vendor
    {
        get { return "David Cumps"; }
    }

    public override string Description
    {
        get { return "This Windows PowerShell snap-in contains the Get-Hello cmdlet."; }
    }
}

} [/csharp]

That's all the code needed to create a Cmdlet! We could install it now, but let's make our lives easier first and configure Visual Studio to support debugging of this Cmdlet. Open the properties of the project and configure the Debug tab as follows:

Visual Studio PowerShell Debug Configuration

Start external program: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe Command line arguments: -noexit -command icmd HelloWorld.dll HelloSnapIn

This will start PowerShell and install the latest version of our Cmdlet whenever we go in Debug mode. When you run it at this stage, it'll throw an error however, because it doesn't know the icmd alias yet. Simply add the following to the PowerShell profile:

[code]

Needed for some commands where administrator permissions are needed

function elevate { $file, [string]$arguments = $args; $psi = new-object System.Diagnostics.ProcessStartInfo $file; $psi.UseShellExecute = $true; $psi.Arguments = $arguments; $psi.Verb = "runas";

$p = new-object System.Diagnostics.Process;
  $p.StartInfo = $psi;
$p.Start();
$p.WaitForExit();
$p.Close();

}

Easily install new cmdlets

function InstallSnappin([string]$dll, [string]$snappin) { $path = Get-Location; $assembly = $path.Path + "\" + $dll; elevate C:\Windows\Microsoft.NET\Framework\v2.0.50727\installutil.exe $assembly | out-null; Add-PSSnapin $snappin | out-null; Get-PSSnapin $snappin; }

set-alias icmd InstallSnappin [/code]

At this stage, going in Debug mode will build the assembly, open PowerShell, execute installutil as an admin, with a UAC prompt under Vista, and install the Cmdlet, after which you are ready to test it and more importantly, use breakpoints! Don't forget to modify the command line arguments in the Debug properties to reflect your assembly name and installer class name when you create a new project.

Let's have a look if we managed to implement our requirements:

[code] PS C:> # Called stand-alone without parameters. PS C:> Get-Hello Hello World!

PS C:> # Called stand-alone with one parameter. PS C:> Get-Hello "David Cumps" Hello David Cumps!

PS C:> # Called stand-alone with multiple parameters. PS C:> Get-Hello "David", "Readers" Hello David! Hello Readers!

PS C:> # Called in a pipeline receiving input parameters. PS C:> Get-Process | Where-Object {$_.CPU -gt 10} | Sort-Object CPU -desc | Get-Hello Hello explorer! Hello dwm! Hello winamp! Hello OUTLOOK! Hello firefox! Hello notepad!

PS C:> # Output its result to the pipeline. PS C:> Get-Hello "A", "C", "B" Hello A! Hello C! Hello B! PS C:> Get-Hello "A", "C", "B" | Sort-Object Hello A! Hello B! Hello C! [/code]

Hooray! We managed to set our development environment up to be productive, being able to easily install the latest version of our Cmdlet and having the ability to debug, and we wrote a Cmdlet offering all the nice features needed to incorporate it into our future scripts. Mission successful I'd say.

As usual, I've uploaded a .zip file which includes the solution to the above Cmdlet, including the Debug properties and the full PowerShell profile I'm using.

What do you think about PowerShell? Will it make your life easier as a system administrator?