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:
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:
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:
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 + "!"
}
}
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:
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:
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:
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:
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:
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: Windows PowerShell