Windows PowerShell: Writing Cmdlets in Script

Don Jones

One of the cool new features in Windows PowerShell v2 is the ability to write greatly improved functions. These functions, written entirely in script, have the same capabilities as a “real” cmdlet written in C# or Visual Basic and compiled in Visual Studio. These advanced functions (they were originally called “script cmdlets” early in the v2 development cycle) help you write more flexible functions you can then use seamlessly alongside regular cmdlets.

It's All in the Binding

The real difference between a mere function and a full cmdlet is that cmdlets support powerful parameter bindings. You can use positional parameters, named parameters, mandatory parameters, and even do basic parameter validation checks—all by simply describing the parameter to the shell. Here’s an example:

Roll Your Own Modules

So how does this help you distribute your scripts more easily? The second type of module, a script module, is the answer. This is simply a normal Windows PowerShell script, with a .psm1 filename extension rather than the usual .ps1 filename extension. Putting mymodule.psm1 into the \modules folder allows you to run Import-Module MyModule, and your script will execute.

Normally, a script module consists entirely of functions. That is, when the module is imported, nothing actually executes—the functions within the script module are loaded into the shell, and become available throughout the shell. Suppose you have a script module that looks something like this:

function Get-Inventory {
    [CmdletBinding()]
    param (
        [parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [string[]]$computername,
        
        [parameter(Mandatory=$false)]
        [alias("PF")]
        [switch]$pingfirst,
        
        [parameter(Mandatory=$true,Position=0)]
        [AllowEmptyString()]
        [string]$class
        
    )
    PROCESS {
    }
}

In this statement, I’ve declared three parameters:

  • computername is a single string or an array of strings. It is mandatory, and it accepts string pipeline input—meaning if you pipe in a bunch of strings, they’ll be automatically dropped into the $computername variable.
  • pingfirst is not mandatory, but if you do use it, you should use the -PF alias. It will save a little typing. This is a switch parameter, meaning it does not accept a value. It’s either on or off.
  • class is also mandatory, but you don’t even have to type the -class parameter name. Just give it the appropriate value as the first-position value when you run the function. Although this is mandatory, it will accept an empty string.

There are many more attributes—and plenty of examples—in the online help. Run help about_Functions_Advanced_Parameters to see them all.

Accessing Common Parameters

The shell defines several common parameters shared by all cmdlets. One of them is -verbose, which is intended to tell a cmdlet to output more information than usual about what it’s doing. The following function definition, however, will result in an error:

function Test-Something {
    [CmdletBinding()]
    param (
        [switch]$verbose
    )
    PROCESS {
    }
}

That’s because you can’t re-define one of the common parameters like -verbose. So how can you tell if your function was run with -verbose or not? Well, it turns out to be unnecessary. Windows PowerShell keeps track of it for you. You simply call Write-Verbose, and Windows PowerShell will ignore those calls if -verbose wasn’t used:

function Test-Something {
    PROCESS {
        Write-Verbose "Starting cmdlet"
    }
}

test-something –verbose

Confirming Impact

Another pair of common parameters is -whatif and -confirm. Any cmdlet that makes some kind of change to the computer is supposed to recognize those. They give you the option of having the cmdlet display what it would normally do (-whatif), or have it individually confirm each action (-confirm). Together, these parameters are called ShouldProcess, and you can declare a function that supports them, as shown here:

function Delete-Things {
    [CmdletBinding(
        SupportsShouldProcess=$true,
        ConfirmImpact="Medium"
    )]
    PROCESS {
    }
}

This declaration enables both -whatif and -confirm as parameters for your function. It also specifies that your function has a "Medium" impact level on the OS. There are no strict guidelines for what "Medium" means—I’d imagine it’s something less than the possibility for total disaster. The real trick is that the shell’s $ConfirmPreference variable defaults to "High." When a cmdlet’s impact is lower than $ConfirmPreference, then the cmdlet will run without confirmation unless -whatif or -confirm are specified.

If the cmdlet’s impact is equal to or higher than $ConfirmPreference, then every time you run the cmdlet, it will act as if you had specified -confirm, even if you forgot to do so. Therefore, if your function is about to do something really dangerous, specify a ConfirmImpact="High" so your cmdlet will always request confirmation. Your other choices are "None" and "Low".

None of the shell’s built-in help actually shows you how to ask for confirmation—and it isn’t automatic. The help refers you to the online MSDN help, which is intended for Microsoft .NET Framework developers and doesn’t reference the shell’s scripting language at all. So I’ll show you here:

function Delete-Things {
    [CmdletBinding(
        SupportsShouldProcess=$true,
        ConfirmImpact="High"
    )]
    Param ($param1)
    PROCESS {
        if ($pscmdlet.ShouldProcess($param1)) {
            Write "Deleting..."
        }
    }
}

Delete-Things "organizationalunit"

$pscmdlet is a built-in variable you can use within the PROCESS script block to access cmdlet-level functionality—including the ShouldProcess method. You pass in a description of what you’re about to modify, and the shell will take care of displaying the actual confirmation or “what if” message.

If ShouldProcess returns $True, then it lets you continue. If it returns $False, then you should not do whatever it is you were about to do. Once you know about the $pscmdlet variable, making sense of those MSDN developer docs is easier. They do accurately describe the different ways you should use ShouldProcess and its companions—like ShouldContinue.

Help! Help! Help!

Don’t forget that functions—even advanced ones—can contain their own built-in help in specially formatted comments, as I described in my March 2010 column. Typically, I list the comment-based help first, then the CmdletBinding statement, then my parameters, and then the BEGIN{}, PROCESS{}, and END{} scriptblocks. It’s alwaysa good idea to include help within your functions—you never know who might benefit from it.

If you’ve written pipeline functions before (also called “filtering functions”), then you already know everything else you need to know in order to write a “script cmdlet.” The PROCESS{} script block is where your code goes, and it will execute once for each object piped into your cmdlet. Everything else about these advanced functions is also just like their somewhat-simpler counterparts.

Windows PowerShell v2 Is Available Now

Although it shipped pre-installed with Windows Server 2008 R2 and Windows 7, Windows PowerShell v2—and its companion Management Framework components—is now available for Windows XP, Windows Server 2003, Windows Vista and Windows Server 2008. Visit support.microsoft.com/kb/968929to get the download link for whatever OS you’re using. This should be compatible with your v1 scripts, and all of my future columns will assume that you’re using 2.0.

A Range of Audiences

The Windows PowerShell team rightly prides itself on making Windows PowerShell useful to a wide range of audiences with varying skill levels. Advanced functions are definitely an example of something that only an advanced audience will find useful.

If you’re just getting started with the shell and have to remind yourself to run Help, then advanced functions are probably still off in the future. You can successfully use the shell without ever writing an advanced function. Once you start getting more advanced, and start writing re-usable components, you’ll find that advanced functions are a great way to do so.

The blog post here is a great example of this: It starts with a simple command that accomplishes a valuable task—something any administrator might write. Then the author gradually expands his command’s functionality into a function, then a filtering function, and finally an advanced function, showing how the shell can scale up as your needs and skills grow.

Don Jones is a founder of Concentrated Technology, and answers questions about Windows PowerShell and other technologies at ConcentratedTech.com. He’s also an author for Nexus.Realtimepublishers.com, which makes many of his books available as free electronic editions.

·      Windows PowerShell: PowerShell and Active Directory

·      Windows PowerShell: Filter Left, Format Right

·      Windows PowerShell: Stay Seated