Building PowerShell Functions – Best Practices

I spend a good deal of time wrapping common tasks into PowerShell functions. Here are a few best practices I’ve picked up along the way.  My apologies if I miss any attributions!

  • Write your function with one purpose.  Don’t build in everything but the kitchen sink.
    • This is one of PowerShell’s strengths – there are existing functions to work with input or output objects, or you can write your own set of functions to work together.
  • Follow naming conventions.
    • Use the Verb-Noun format, use an approved verb, and ensure that your Noun is unique and will not collide with another author’s function now or in in the future.
      • Approved Verbs
      • Example:  I’m writing commands to work with a Hyper-V lab.  Set-Lab and Get-Lab are generic and may be used by another author.  I can add a prefix like HV to avoid this – Set-HVLAB and Get-HVLab
    • Use common parameter names and types as appropriate.
      • Example:  Use ComputerName to specify systems.  Do not use ComputerNames, Computer, PC, or any other non-standard parameter name.  If desired, provide an alias for the parameter.
  • Use the built in comment-based help system.  At a minimum, provide a helpful synopsis, description, parameter (for all), and example
  • Let PowerShell do the work for you.  Always use [cmdletbinding()], which allows you to take advantage of the following:
  • Use advanced parameters for validation, accepting pipeline input, specifying mandatory parameters, and other functionality, where possible and appropriate.
  • Provide flexibility with your parameters.  Provide default values, allow arrays instead of single objects, allow wildcards, and provide other helpful parameter features.
    • Example:  [string[]]$ComputerName = $env:computername is more helpful than [string]$ComputerName
  • Document your code for yourself, readers, and users.
    • Use write-verbose, write-debug and write-error to provide insight at the shell
    • Comment your code in everyday language for readers.  If you used a specific command or logic for a reason, explain why it was necessary and why changing it could break things.
    • Use full command names and full named parameters.  This makes the code more readable.  It also prevents issues that could arise if you rely on aliases or positional parameters.
  • Avoid dependencies.  This includes external scripts and modules, binaries, or features exclusive to PowerShell or .NET Framework versions.  If you must include dependencies, be sure to indicate this and provide appropriate error handling.
    • Example:  Get-ADGroupMember requires the ActiveDirectory module.  Instead of relying on this, include or write your own function.
    • Example:  To create a new object, use New-Object -TypeName PSObject -Property @{A=1; B=”Two” } | Select-Object A, B instead of [PSCustomObject]@{A=1; B=”Two”} to provide compatibility with PowerShell 2.
  • Provide error handling with helpful messages
  • Do not break the user’s environment.  Don’t touch the global scope.
  • Test, Test, Test!  Test any reasonable scenario your function might run under.
    • Test with and without a profile.  Test with 64 and 32 bit PowerShell hosts.  Test with the ISE and Console Host.  Test with a single-threaded apartment and multi-threaded apartment.  Test with and without the administrative token, with and without actual administrative authority.
  • If your function provides output, use objects.
    • Do not output strings.  Do not use Write-Host.  Do not format the results.  You and your users will get the most out of PowerShell when you provide output in objects, that can be passed down the pipeline to other commands.
    • Creating Custom Objects

Why bother?

  • Following these best practices will help you and the greater PowerShell community if you chose to share your code.
  • Your function will fit into the PowerShell world, enabling integration with the many technologies PowerShell can work with.
  • Your function will be usable by wider audiences, who may even provide suggestions and tweaks to help improve it.
  • Your function will be flexible and gracefully handle various scenarios you throw at it.
  • Your function will last.  If you avoided or accounted for dependencies, your function should withstand changes to PowerShell, the .NET Framework, and the user’s environment.
  • These practices apply to scripts as well.  You can use the majority of these best practices when writing scripts, rather than functions.

Illustrating the best practices

We will look at Get-InstalledSoftware, a quick function that extracts installed software details from the registry.

Write your function with one purpose.

This function does one thing: get installed software.

Follow naming conventions.

Get-InstalledSoftware follows the Verb-Noun naming format, uses an approved verb, and uses typical parameter names such as ComputerName… but the function name is not unique.  In fact, there is another script out there with the same name.  Perhaps I should have chosen a better example!

Use the built in comment-based help system.

The help system provides a synopsis, a description that points out prerequisites, describes each parameter, provides two examples, and provides a link that will take the user to the Technet Gallery page if they use Get-Help Get-InstalledSoftware –Online

Let PowerShell do the work for you.

The function uses [cmdletbinding()] and many of the features it enables.

Use advanced parameters

This function uses advanced parameters for computername.  This allows input from the pipeline (e.g. an array of strings), input from the pipeline by property name (e.g. an array of objects with a computername property), and validates that the argument is not null or empty.

Provide flexibility with your parameters.

ComputerName is given a default value of the local machine and allows an array of strings rather than a single string.  The Publisher and DisplayName parameters are used with the –Match operator and can thus take in regular expressions.

Document your code for yourself, readers, and users.

The code uses Write-Verbose and Write-Error.  Comments explain what is happening.  The ‘help’ information describes prerequisites, and if connectivity fails, verbose output suggests where to start troubleshooting.  Aliases are not used in the function.

Avoid dependencies.

This code does depend on certain factors – privileges, connectivity, and the Remote Registry service.  This is detailed in the help information and in the verbose output.  Language, including the output objects we create, is compatible with PowerShell v2.

Provide error handling with helpful messages

Try/Catch blocks are used to capture errors where they would likely occur, and are used in a way that will allow continued processing if errors occur.  For example, if multiple computers are specified and one fails, we move to the next computer (continue), rather than breaking execution of the command.

Do not break the user’s environment.

The global scope is not altered by this function

Test, Test, Test!

This script was tested in a limited number of expected scenarios.  With and without a profile.  In the ISE and console host.  With and without the administrative token.

One scenario that illustrates the importance of testing is this command’s behavior in a 32 bit session on a 64 bit machine.  In this scenario, the script will miss 64 bit items, and will pull double copies of everything else (the native and Wow6432Node keys will point to the same location).  I added this to the description.  Ideally I should test for and handle this, but doing so would add undue overhead to a lightweight function for what I consider a niche scenario.

If your function provides output, use objects.

This function provides object based output.  Not text.  Not a CSV.  You can use the output with any number of built in or custom commands.

Get-InstalledSoftware in action

  • The end user can use the built in Get-Help command for help.  The online switch takes you right to the TechNet gallery site.

image

image

  • We can pass in multiple computers and filter Publisher and DisplayName using regular expressions

image

image

Helpful resources

The following resources will provide further help and suggestions for best practices when writing PowerShell.

Good luck!  If you do end up writing advanced functions, please consider posting them to websites like PoshCode, TechNet Script Gallery, CodePlex, or GitHub!

Advertisements

7 thoughts on “Building PowerShell Functions – Best Practices

  1. Great article! I like that you cover a nice range of items for advanced functions and even provide an example using one of your own functions. I know looking at some functions posted to various sites along with judging the Scripting Games that this kind of article is needed to help steer others more towards writing better functions. I went and covered pipeline support in depth because of what I saw during the recent Scripting Games here: http://learn-powershell.net/2013/05/07/tips-on-implementing-pipeline-support/

  2. Checked out your example and I really liked that you piped the custom psobject through a select statement to force the order of the attributes in the results (that the prior hash definition used in new-object otherwise mangles). Not certain why I’ve not been doing that in my own functions but I’ll start doing that now :)

    Great article!

  3. Pingback: SQL For PowerShell, For SQL Newbies | rambling cookie monster

    • Much appreciated Stephen, enjoying your writing as well, hope to see more!

      And thanks for the reminder, noticed a decent write-up on simple and advanced functions from Ed earlier this year, just added it to the resources list.

  4. Re: your example under test, test, test.
    You could write a function to detect the ‘bitness’ of the OS, and have it incorporated as a dependency. I can easily see the bitness function being a common feature of your testing suite.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s