Saturday, March 22, 2008 9:09 AM bart

Windows PowerShell 2.0 Feature Focus - Script cmdlets

Two weeks ago I did a little tour through Europe spreading the word on a couple of our technologies including Windows PowerShell 2.0. In this blog series I'll dive into a few features of Windows PowerShell 2.0. Keep in mind though it's still very early and things might change towards RTW - all samples presented in this series are based on the CTP which is available over here.

 

Introduction

In this first post we'll take a look at script cmdlets. Previously, in v1.0, the creation of cmdlets was an exclusive right for developers using any managed language (typically VB.NET or C#). I've been blogging about this quite a bit in the past all the way back to May 2006:

To work around this limitation, lots of IT Pros have been writing PowerShell scripts that take the naming pattern of cmdlets but the invocation syntax of those is completely different than real cmdlets. For example, there's no built-in notion of mandatory parameters to scripts unless you write your own validation. Similarly, things such as -whatif and -confirm are not supported but these scripts.

Starting with PowerShell 2.0, the creation of cmdlets is now possible using script as well. In this post, I'll port my file hasher cmdlet to a script cmdlet.

 

The basics

Creating a script cmdlet starts by creating a script file, e.g. get-greeting.ps1. Below is the skeleton of a typical script cmdlet:

Cmdlet Verb-Noun
{
   Param(...)
   Begin
   {
   }
   Process
   {
   }
   End
   {
   }
}

The minimalistic script cmdlet would simply consist of a Process section, like this:

Cmdlet Get-Greeting
{
   Process
   {
      "Hello PowerShell 2.0!"
   }
}

In order to execute, save the file (e.g. get-greeting.ps1) and load it using . .\get-greeting.ps1. Now the get-greeting cmdlet is in scope and can be executed:

image

If the cmdlet is executed as part of a pipeline, which means (possibly) multiple records that are flowing through the pipeline have to be processed, the Process block will be executed for each of those. However, the Begin and End blocks will be triggered only once. Before we can go there, let's take a look at parameterization.

 

Parameterization

Parameterization is maybe the most powerful thing about script cmdlets. It all happens in the Param section. Let's extend our greeting cmdlet with a parameter:

Cmdlet Get-Greeting
{
   Param([string]$name)
   Process
   {
      "Hello " + $name + "!"
   }
}

Perform the same steps to load the cmdlet and execute it, first without arguments, then with an argument:

image

The first invocation is not really what we had in mind. The parameter needs to be mandatory instead. In script cmdlets, this is easy to do, simply by adding an attribute to the parameter:

Cmdlet Get-Greeting
{
   Param([Mandatory][string]$name)
   Process
   {
      "Hello " + $name + "!"
   }
}

Now, PowerShell will enforce this declaration and require the parameter to be supplied:

image

Here you see how the PowerShell engine takes over from the script author. Beyond simple mandatory parameters, on can specify validation attributes as well, such as AllowNull, AllowEmptyString, AllowEmptyCollection, ValidateNotNull, ValidateNotNullOrEmpty, ValidateRange, ValidateLength, ValidatePattern, ValidateSet, ValidateCount, ValidateScript. The latter is interesting in that it is not available to managed code cmdlets at the time being - it allows a script function to be specified to carry out validation of the parameter's value (e.g. a script that validates ZIP codes or SSN numbers, that can be reused across multiple script cmdlets).

 

The pipeline

Let's make our cmdlet play together with the pipeline now. We're already emitting data to the pipeline, simply by our "Hello" ... expression that produces a string. However, we'd like to grab data from the pipeline too. This can be done by binding a parameter to the pipeline:

Cmdlet Get-Greeting
{
   Param([ValueFromPipeline][Mandatory][string]$name)
   Process
   {
      "Hello " + $name + "!"
   }
}

image

Here the strings "Bart" and "John" are grabbed from the pipeline to be bound to the $name parameter. To show that Begin and End are only processed once, change the cmdlet as follows:

Cmdlet Get-Greeting
{
   Param([ValueFromPipeline][Mandatory][string]$name)
   Begin
   {
      Write-Host "People can come in through the pipeline"
   }
   Process
   {
      "Hello " + $name + "!"
   }
   End
   {
      Write-Host "Goodbye!"
   }
}

and the result is:

image

Typically Begin and End are used to allocate and free shared resources for reuse during record processing.

 

Interacting with the pipeline processor

There's still more goodness. Using the $cmdlet variable inside the script cmdlet, one can extend the capabilities even more. To see what this can do, create a simple script cmdlet:

Cmdlet Get-Cmdlet
{
   Process
   {
      $cmdlet | get-member
   }
}

This is the result:

image

We won't be able to take a look at each of those, but let's play with a couple of those: ShouldProcess and WriteVerbose.

Cmdlet Get-Greeting -SupportsShouldProcess
{
   Param([ValueFromPipeline][Mandatory][string]$name)
   Begin
   {
      #Write-Host "People can come in through the pipeline"
   }
   Process
   {
      if ($cmdlet.ShouldProcess("Say hello", $name))
      {
         $cmdlet.WriteVerbose("Preparing to say hello to " + $name)
         "Hello " + $name + "!"
         $cmdlet.WriteVerbose("Said hello to " + $name)
      }
   }
   End
   {
      #Write-Host "Goodbye!"
   }
}

Notice the addition of -SupportsShouldProcess in the Cmdlet declaration. This tells the engine our cmdlet is capable of supporting -whatif and -confirm switches. Inside the implementation we add an if-statement that invokes ShouldProcess specifying the action description and the target ($name). The result is this:

image

Essentially, -whatif answers that ShouldProcess call with false, skipping the real invocation but still printing the actions and targets the operation would have triggered. When using -confirm, the user is prompted each time (unless [Yes|No] to All is answered obviously) a ShouldProcess call is made.

When using the -verbose switch, the WriteVerbose calls are emitted to the console as well:

image

 

Porting the File Hasher cmdlet

Enough introductory information, let's do something real. Here's the script for my old file hasher cmdlet ported as a script cmdlet:

Cmdlet Get-Hash
{
   Param
   (
      [Mandatory][ValidateSet("SHA1","MD5")][string]$algo,
      [Mandatory][ValueFromPipelineByPropertyName][string]$FullName
   )
   Begin
   {
      $hasher = [System.Security.Cryptography.HashAlgorithm]::Create($algo)
   }
   Process
   {
      $fs = new-object System.IO.FileStream($FullName, [System.IO.FileMode]::Open)
      $bytes = $hasher.ComputeHash($fs)
      $fs.Close()

      $sb = new-object System.Text.StringBuilder
      foreach ($b in $bytes) {
         $sb.Append($b.ToString("x2")) | out-null
      }

      $sb.ToString()
   }
}

Pretty simple, isn't it? A few implementation highlights:

  • I have two parameters, comma-separated in the Param(...) section.
  • The first parameter should either be MD5 or SHA1 (case-insensitive), which I'm validating using ValidateSet. Anything but those two will fail execution of the cmdlet.
  • The second parameter is taken from the pipeline by property name. Notice FullName is a property on file objects, so this allows to pipe the output of get-childitem (dir) in a file system folder to the get-hash cmdlet.
  • Creation of the hasher algorithm is straight-forward but is done in the Begin section to allow reuse across multiple processed records.
  • The core of the implementation is simple: it opens the file as specified in the $FullName parameter, feeds the stream into the hasher and turns the bytes into their string representation. Notice the use of out-null to suppress any output from the $sb.Append call to bubble up to the pipeline, only the $sb.ToString() result is reported.

Here's the result:

image

Hashes are calculated for all *.cs files. I didn't extend the sample to print the file name (would be simply to do) or to report it as part of the output (wrapping a file name and the hash result in an object, which is harder to do) but if you go back to my original file hasher cmdlet post, you'll see there's another option using the Extended Type System.

Enough for now. As you saw in this post, script cmdlets unlock an enormous potential to extend PowerShell with first-class citizen cmdlets simply by leveraging your scripting knowledge in PowerShell. Together with some other features such as script internationalization (coming up in this series) and packages and modules (not in the current CTP) this is just the tip of the iceberg of PS 2.0 Production Scripting.

Happy script-cmdlet-ing!

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

Filed under:

Comments

# Dew Drop - March 22, 2008 | Alvin Ashcraft's Morning Dew

Pingback from  Dew Drop - March 22, 2008 | Alvin Ashcraft's Morning Dew

# PowerShell 2.0 : création de cmdlets en script

Monday, March 24, 2008 8:26 AM by CoqBlog

Avec PowerShell 1.0, le développement de cmdlets se fait obligatoirement par un langage managé comme

# re: Windows PowerShell 2.0 Feature Focus - Script cmdlets

Tuesday, March 25, 2008 11:03 AM by Catweazle

Useful information - thanks! I get the impression there is no mechanism for a script cmdlet to be "registered" with the PowerShell system, like Add-PSSnapin for .NET snap-ins? For layering an MMC snap-in on top of PowerShell presumably it's best to stick with .NET cmdlets so that an installer (MSI) can easily register those cmdlets for use by the MMC. With script cmdlets I imagine the MMC would need to know the installation path for the file containing the script cmdlets and somehow perform the initial load?

# re: Windows PowerShell 2.0 Feature Focus - Script cmdlets

Tuesday, March 25, 2008 11:20 AM by bart

Hi Catweazle,

Although it's a little too early in the PS 2.0 development cycle to make a real judgement on this, I'd say the typical .NET-based snap-ins are likely a better candidate for such layering. There are various reasons for this, one is installability (although packages and modules - another planned 2.0 feature - might remove such restriction partially) another is your IP (you might now want everyone to see / being capable to change how the cmdlet is implemented - agreed there is Reflector too :-)) and the complexity of the code which might be better expressed in managed languages than in script. Personally I think of script cmdlets as an extension to scriptability for admins and less as an extensibility point for snap-in vendors.

Hope this helps,

-Bart

# re: Windows PowerShell 2.0 Feature Focus - Script cmdlets

Wednesday, March 26, 2008 1:10 PM by Jason Archer

It will be great to script cmdlets/functions with more built in functionality. I noticed a bug in your file hasher. It does not specify an access mode when opening the file, and instead defaults to ReadWrite. This generates an exception when you try to get the hash of a read-only file.

# re: Windows PowerShell 2.0 Feature Focus - Script cmdlets

Wednesday, March 26, 2008 1:50 PM by bart

Hi Jason,

Thanks for your feedback and for noticing the bug.

Cheers,

-Bart

# Windows PowerShell 2.0 Feature Focus - Script Debugging

Wednesday, March 26, 2008 10:18 PM by B# .NET Blog

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

# re: Windows PowerShell 2.0 Feature Focus - Script cmdlets

Saturday, March 29, 2008 9:01 AM by Hal Rottenberg

Bart,

We highlight this article in the upcoming PowerScripting Podcast Episode 22.  Look for it to be released at the above URL in the next 48 hours.

# Windows PowerShell 2.0 Feature Focus - Script cmdlets

Sunday, March 30, 2008 1:22 PM by DotNetKicks.com

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# Episode 22 – The One About Brandon « PowerScripting Podcast

Pingback from  Episode 22 – The One About Brandon «  PowerScripting Podcast