I had anticipated that I would be writing this second part about providers rather than design principles, but after listening to a recent Powerscripting Podcast I thought it would be helpful for me to provide a few things I’ve learned from developing the vWorkspace PowerShell module. Many of the points brought up during the podcast were that developers often have no experience with PowerShell so they do not fully understand how a cmdlet or module should be developed. I hope this post can provide some insight into my endeavor.
Develop a Module
Snap-ins were a 1.0 concept that was a step in the wrong direction. A lot of companies developed snap-ins to get on the PowerShell band wagon and now are paying the price because snap-ins are becoming a second class citizen. In PowerShell 3.0, Microsoft didn’t even release any snap-ins, everything is a module. A module is much easier to work with and can easily xcopy deploy. The ability to use a module manifest makes it even easier to customize the user experience or brand it.
Use the pipeline correctly
Cmdlets that can process a collection of items should always accept pipeline input. The user should not have to use a ForEach-Object to use your cmdlet with a collection. Take for instance the Get-Process and Stop-Process cmdlets. Notice that they work together to provide a seamless pipeline experience.
Get-Process VMM* | Stop-Process
If Stop-Process would have been poorly designed the command would look like this.
Get-Process VMM* | ForEach-Object { Stop-Process $_ }
This is a horrible user experience. This is also not what a PowerShell user expects from a cmdlet. Whenever I see a pair of cmdlets with the same noun, such as a Get and then a Set,Invoke,Stop, etc, I expect the cmdlet taking action (the latter), to accept pipeline input from cmdlet writing to the pipeline (the former). There are two ways (of three total) that users will typically use pipeline input. The most common is to simply accept pipeline input of the object as a whole. In the example of Get-Process, the ProccessInfo object is written to the pipeline and the Set-Process cmdlet consumes the object as a whole.
Get-Process -> ProcessInfo -> Set-Process
The second type of pipeline input that is commonly, although less than the first, is pipeline input via property name. In this circumstance, a single property of the pipeline object will be provided to the consuming cmdlet. For example, let’s create a new related cmdlet called Out-Id. Since an Id is not process specific, we want the cmdlet to accept an Id property from any type of object.
[Cmdlet(VerbsCommon.Out, "Id")]
public class OutIdCommand : Cmdlet
{
[Parameter(ValueFromPipelineByPropertyName=true)]
public object Id {get;set;}
public override void ProcessRecord()
{
WriteObject(Id);
}
}
Whenever an object is piped to the Out-Id cmdlet, it will read the Id property from the object and set the Id parameter of the Out-Id cmdlet. It them simply writes that Id to the pipeline.
Get-Process -> ProcessInfo.Id -> Get-Id -> Id
I think this type of pipeline input it undervalued and underused. I would love to seem some cmdlets developed that really harnessed this. I, for one, have never successfully created a cmdlet that could.
The final type of pipeline acceptance is value by remaining arguments. I think it gets its start from the params keyword in C#. It will consume the remaining parameters and pass them to the specified pipeline parameter. Again, I have never used this but in this circumstance I can’t say I’ve ever seen it used.
Write output to the pipeline correctly
The pipeline is intended to be serial. Rather than outputing collections to the pipeline, a single object should be written at a time. This means that if our cmdlet is to output a collection of objects, we need to loop through each one and write each one using WriteObject.
public override void ProcessRecord()
{
MyObject myObjects[] = GetMyObjects();
foreach(var obj in myObjects)
{
WriteObject(obj);
}
}
To clean up my cmdlets, in my base class, I created a simple method called WriteObjects.
protected void WriteObjects(IEnumerable objects)
{
foreach(var obj in objects)
{
WriteObject(obj);
}
}
Now I can change the above code to this.
public override void ProcessRecord()
{
MyObject myObjects[] = GetMyObjects();
WriteObjects(myObjects);
}
Consider using Type Formatting XML for objects
Files with the PS1XML extension can be used to format your custom types into a more user friendly experience. Rather than dealing with 30-40 properties on an object, a user will see the ones that are most important. If they wish to get more information, they can simply use the Format-List * command to get all the properties. Depending on your situation, it also gives you the opportunity to add methods or properties that will only make sense on the command line. The System.Management.ManagementObject formatting is a great example of this. The date and time used by WMI is impossible to understand without conversion. The type formmatting adds a simple method to the ManagementObject class to provide a human readable interpretation.
<Name>System.Management.ManagementObject</Name>
<Members>
<ScriptMethod>
<Name>ConvertToDateTime</Name>
<Script>
[System.Management.ManagementDateTimeConverter]::ToDateTime($args[0])
</Script>
</ScriptMethod>
For examples of type formatting look in the PowerShell installation directory. There are all kinds of examples of what Microsoft has done to make particular .NET objects work better on the command line.
C:\windows\system32\windowspowershell\v1.0\
Consider using built in Aliases for cmdlets
Microsoft has many built in aliases for their important cmdlets. For instance Get-Command is gcm or Get-Process is gps. The vWorkspace team decided to create aliases for every cmdlet. I am not entirely convinced this was the best idea but I still think it is helpful. There is some discussion as to whether this is best practice. I recommend reading Kirk Munro’s post on the topic. Always make sure that another alias does not conflict with the one you are creating.
Accept multiple types for input
PowerShell is very forgiving when it comes to actual .NET types. Types can be cast to other types. Consider reading this blog post about PowerShell casting to see how it performs that conversion. When exposing parameters take these kind of conversions into consideration. Being able to type a date as a string rather than using some sort of .NET class or cmdlet is very time saving. If you wish to accept a complex type, consider creating a ArgumentTransformationAttribute on the parameter. This will allow you to write some custom code to convert the user input into the type you are expecting. This makes working with your cmdlets much, much easier.
Validate Early
Consider using ValidationAttributes on your parameters so that input doesn’t even make it past the PowerShell runtime. Using a generalized validation attribute can make the user’s workflow much more predictable. It also makes learning your cmdlet easier. Instead of “now, what does this error message mean…” it will be “oh, I’ve seen this error message before, I have to do this.” Check the list of built in validation attributes before making your own.
Don’t put parameters on base classes
I think this rule may be not always be true but I have found that putting a parameter on the base class, even when you KNOW that every cmdlet will need it, still has some negative side effects. The biggest one for me was the fact that there is no good way to set the positional argument of the ParameterAttribute.
Use Dynamic Parameters Sparingly
Dynamic parameters are cool but mostly for developers. As a developer I feel like there is always a mentality to create a super generic, reusable component and I see this taken too far. Dynamic parameters are kind of like this. It would be cool to have a single class that defines the parameters for both your New and Set but it makes it way harder to understand how to use your cmdlet. You don’t get any syntax help from Get-Help. That’s a deal killer for me.
The one place were I would use dynamic parameters would be within a provider to add additional parameters to cmdlets like Get-Item. The parameters still need to be documented in the provider documentation, but users expect this because that is how some of the pre-existing providers work. I still don’t completely agree with these “hidden” parameters but they are useful.
Make your cmdlets testable
Test driven development is all the hype now! While I’m not a loyal follower of the ideology, I still think that unit testing is required to create good software. If you develop your cmdlets directly on top of the PowerShell subsystem, it may be hard to mock, inject and test later on. Consider wrapping the SessionState, WriteObject, ShouldProcess and other underlying Cmdlet and PSCmdlet members with a layer of interfaces to allow for easy testing later on. Most of the time there should not be that much code in your cmdlet, and more in your business object, but there will always be more logic than you realize and testing it will be necessary. Put a little time in now to save yourself later.
Sign Your Scripts
If you are producing a module, make sure you sign your scripts. It really wraps up everything and gives it that professional seal. It’s a pain in the but so I use the Script Signing add-on for PowerGUI. It makes it way easier. Once it’s configured, I can just open up a script in the editor, click one button to sign the file.
Document Everything
By far the most important documentation you can provide is the built in cmdlet documentation. Get-Help is the crutch of every PowerShell user. I use it for cmdlets that I have used 100 times. Make sure to get the Cmdlet Help Editor to make generating the XAML file. It can be finicky so save often. In addition to cmdlet help the vWorkspace module objects are also documented on a Wiki. This help is pulled right from the XML help found in the source code and put into a WikiMedia import XML file so it can just be imported right into the Wiki. Every build we have a new wiki generated on the file that has all the object and cmdlet help. If it pertains to your module, provide about_*.txt files to document core concepts that can span pages.
I do not believe this encompasses every best practices or that all these are even best practices. This is what I have found has worked for our development process. I would love to hear some feedback about what you have done during the development of cmdlets and modules. I hope to extend this list as I discover new things or think of something to add to the list. I still plan on doing a part on providers.