One of the most common tasks in PowerShell is writing PowerShell functions. Functions are one of the basic building blocks we use to separate and abstract our code away. Without them, our scripts would be just a tangled mess of
for loops, and duplicated code.
Functions allow us to package our PowerShell logic into discreet blocks we can call, pass parameters to affect how they work, and reuse them so we can follow DRY (Don't Repeat Yourself) principals.
PowerShell has a lot of functionality tucked away into functions that sometimes are not known, ignored, or forgotten about entirely. Let's talk about some basic things we can add to functions that improve our scripts and ultimately make us better tool makers.
- Tip 1: Functions Should Do One Thing
- Tip 2: Functions Should Be Testable
- Tip 3: Functions Should Be Self-Contained
- Tip 4: Add Comment-Based Help
- Tip 5: Use The PowerShell Function Naming Convention
- Tip 6: Leverage Advanced Functions
- Tip 7: Support the Pipeline
- Tip 8: Support -WhatIf If Making Changes
- Tip 9: Support -Confirm if Making Changes
First off, let's make one thing clear. Functions should do one thing and one thing only.
Give me a ping Vasili, one ping only please.
- Capt. Marko Ramius - The Hunt for Red October
I've seen countless PowerShell functions that try to cram in entirely too much logic and end up being an unwieldy mess. These large, unfocused functions end up being hard to understand as they have no clear purpose. They can perform in strange ways, utterly unrelated at all to what the user thinks the function does.
I'll admit, I do this myself sometimes (hey nobody's perfect). This is especially true when you're writing code for the first time to perform a new task. It's easy to fall into the trap of handling this one edge case here, this other edge case there, maybe output this variable if these set of conditions are true, etc.
We must recognize when this happens, and to correct the situation as soon as possible. If we don't, we'll end up struggling to support an unmaintainable function, our productivity suffers, and the users of our code become frustrated at the lack of focus.
In short, single-purpose PowerShell functions are easier to write tests for in tools like Pester. We can create tests for the (hopefully one or just a few) different scenarios of expected input parameters and validate the function produced the expected output. If the function does too many things, writing unit tests becomes difficult or almost impossible.
It's better to have a handful of small, discreet functions with quality unit tests than one large function with no or poor unit tests.
It's almost a certainty that your PowerShell function is working with variables in some way. A good practice is to supply the function with all the external variables it may need to perform its task as parameters into the function.
If our function only reads external variables, it's a good idea to add parameters to the function, with the default value being the external variable.
A big reason to do this is it helps make writing unit tests for functions easier.
We can confidently test a function in isolation, and verify the outputs if we supply all the required information directly into the function.
When functions access external variables and make assumptions on their contents, hard to troubleshoot bugs can occur if those variables had their contents modified but another function.
PowerShell has this excellent feature called comment-based help. Use it to provide users of your code clear guidance on what it does and how to use it.
When PowerShell parses your function, the comment-based help contained within is accessible via the builtin Get-Help cmdlet. The help text for a function doesn't need to be a novel, but clear and accurate help on what the function does, descriptions for each parameter, and useful examples for how to use the function is one of the best ways to delight the users of your function. Conversely, having no or inaccurate help is one of the easiest ways to frustrate the users of your software with the genuine possibility of them stopping to use it at all.
PowerShell uses a
Noun syntax for functions and cmdlets. You can see the list of approved verbs with descriptions about what they are intended for by running the
A big reason for the approved verb list is consistency. The creators of PowerShell wanted it to provide a consistent experience to the user. This consistently also enhances PowerShell's readability.
If a function is called
Get-IpAddress, there is little ambiguity as to that the function does. It returns an IP address. It does not set the IP address or otherwise make changes to the system.
If we wanted to set the IP address, it would be intuitive to have a
I've seen many custom PowerShell functions use the
Get verb but internally make non-obvious changes to the system. This not only causes confusion because you're wondering: "Hey! I just got my IP address. Why did my recycle bin get cleared out?" It also has the potential to cause harm, as the actions of the function does not match what the user expects.
This is a big no-no in PowerShell.
PowerShell gives you a excellent set of features you can use in functions for practically free if you make them advanced functions. By adding the [CmdletBinding()] attribute to a function, you now have access to a set of common parameters you can utilize for things like:
- Writing verbose messages
- Parameter validation
- Support input from the pipeline
- Advanced parameters
When creating new functions, it is a good practice to make them advanced functions as a matter of course. This allows you to support the features above easily should you need them.
This is an extension of Tip #6, but I feel it deserves its own section. If you're adhering to Tip #1 and writing functions that do one thing only, then supporting the pipeline is an important feature to add so functions can be chained together to perform more complex logic.
Adding this argument indicates to PowerShell that the value of the parameter can come from a pipeline object. This allows you to stream the value of the parameter from the output of another function/cmdlet.
For more information about the PowerShell pipeline, check out the about_pipelines help doc or run
Get-Help about_pipelines from PowerShell itself.
PowerShell's support of the
-WhatIf switch parameter is analogous to
--noop in other tools. When specifying the
-WhatIf switch, what you're essentially telling PowerShell is: "Tell me what you're going to do, but don't actually do it."
It's a great tool to have when running PowerShell interactively to validate a series of actions that could potentially have serious effects without actually making them.
Imagine you needed to run a command to delete files from a directory recursively. You want to delete all
.log files from the
C:\tmp directory but also want to be extra careful that you don't inadvertently delete other files, so you add the
-WhatIf switch to
Remove-Item like so:
Get-ChildItem -Path C:\tmp -File *.log -Recurse | Remove-Item -WhatIf
With this, you can see all the files that
Remove-Item would have deleted if you hadn't provided the
-WhatIf switch. After validating the files displayed, you safely remove the
-WhatIf switch and rerun the command to remove the files.
-WhatIf is only supported in advanced functions, so it’s another reason to implement Tip #6. When authoring functions that affect the system in some way, add
-WhatIf support via the
SupportsShouldProcess argument on the [CmdletBinding()] attribute.
Related to Tip #8, the
-Confirm switch indicates to PowerShell to pause before every action and display an interactive prompt asking you to confirm the action before it executes. It will do this for every object in the pipeline, allowing you to inspect the item before continuing.
Just like with
-WhatIf support, adding
-Confirm support is only possible with advanced functions, so Tip #6 still applies.
One of the problems with making a list of tips is knowing when to stop. There are many more tips I could have added about writing functions or even a list of things to definitely not do. Those will have to wait for another day.
Icon courtesy of Tip Icon #388700