DEV Community

Cover image for How to write great PowerShell functions!
TheInfraDev
TheInfraDev

Posted on • Originally published at theinfradev.pro

How to write great PowerShell functions!

So far I have talked about the InfraDev's mission, and what languages and tools you might use, so now I think it is time to look at some concrete examples of how people commonly write PowerShell functions, and then how to improve them to be more stable, readable, and maintainable while living up to the standards of the Powershell community. One of the key skills in an InfraDev’s life, is the ability to write functions. Functions allow us to:

  • Re-use and share code by making standard implementations.
  • Encapsulate and therefore hide our implementation (not for security, but readability).
  • Make clear Entry and Exit points to different behaviours.
  • Change a swiss army knife into a hammer

Let's start with a common way people write functions

I have heard it is good to start at the beginning

I have chosen a rather simple and common form of a function. We are going to provide config data (Computername) and then return services data narrowed down to just the Name and Status properties.

$GLOBAL:ComputerName = 'Server01'

Function Get-ServiceData

    {
      Get-Service -ComputerName $ComputerName | Select-Object -Properties Name,Status
    }

Get-ServiceData

    So what are the key features of this Function?

  • $Computername is out of scope and so is declared as a Global variable. This is a very fragile and just all-round bad practice. Bad Coder! Bad!
  • The data we are returning is instead printed to the console (sometimes called Stdout). This is bad for the purposes of extending the use of the function. In fact our function is not returning any data.
  • If get-service fails for any reason - we get an error, but nothing returns to our console (std out). Worse, because we were using write-host, we have no way of checking what happened! This is not good if we ever want to use this as a chain of functions or check programmatically whether the function succeeded or failed.
Things you might not know: "What is StdIn / StdOut?" (Linux information project definition) Standard output, sometimes abbreviated stdout, refers to the standardized streams of data that are produced by command line programs (i.e., all-text mode programs) in Linux and other Unix-like operating systems. These standardized streams, which consist of plain text, make it very easy for the output from such programs to be sent to a devices (e.g., display monitors or printers) or to be further processed by other programs." more...

Let's make this function better

It's time to get technical?

$ComputerName = 'Server01'

Function Get-ServiceData ($ComputerName)

    {
      $Service = Get-Service -ComputerName $Computername

      Return $Service

    }
$RemoteServices = Get-ServiceData -ComputerName $ComputerName
  • $ComputerName is in scope as we are passing it into this function as a parameter. This not only stops the global variable issue, but also means that other people using this function don’t have to magically know that they must declare this variable first.
  • The Get-Service data is now added to a variable and is properly returned as output of this function. This provides for re-use of the output.
  • Further, we are also capturing the Return of our function in the $RemoteServices variable which we can then either evaluate and use further or just write to the console.
  • We haven’t fixed all the flaws, because If Get-Service fails for any reason - we still return nothing and anything relying on the return of this function is out of luck.

Is this best implementation? No, this is still a fragile implementation, but we have resolved some issues around returning the data and passing in data

Let's go all the way to stable, maintainable code

This won’t be javascript then j/k ;)

$ComputerName = 'Server01'

$RemoteServices = "No data"

Function Get-ServiceData

    {
      [CmdletBinding(SupportsShouldProcess)] Param([string]$ComputerName)

      $Service = "Unable to retrieve data"

      $Service = Get-Service -ComputerName $Computername

      Return $Service

    }

$RemoteServices = Get-ServiceData -ComputerName $ComputerName

  • $Computername is in scope as we are passing it as a parameter into this function.
  • We have also given ourselves extra PowerShell cmdlts such as -whatif and -confirm through using the [CmdletBinding(SupportsShouldProcess)] syntax. Rather than the () we used before, now we are using best practice and changing our function into a proper 1st class PowerShell cmdlet.
  • We declared that the input data is a string. This provides a type check to make sure we are giving the right type of variable and if not, it will cast it as a string anyway. While the function may fail to provide us the data we want, it will provide data.
  • Get-Service data is added to a variable that is already initialised. If Get-Service fails to retrieve any data, our original data will be kept. If it succeeds our data will be overwritten by the object returned.
  • If Get-Service fails for any reason - we get an error message.
  • Finally $RemoteServices is also initialised as a string 'No data' which is no longer a null or undefined variable. This also means if Get-Service fails we will then have our error declared and ready to be returned. Therefore, we know that if we get this back as the return, the Get-Service cmdlet failed.

Let’s wrap this up

Finally, some good news!

Each of these implementations would have worked and without rigorous testing we might have even considered them working functions. Up until they didn't, and the wrong data was fed in, or the wrong data came back. What I have tried to show here is how we can use a couple of key principles to provide stability to our functions.

Yes, the function is longer, and yes we have also nailed down a few concrete details such as variable types which is perhaps not in the dynamic nature of PowerShell - but by doing this we have made this function safer in the overall context and we have also made sure our hypothetical tool wouldn't error out due to nulls or failures in the implementation itself. Instead when we get bad data, we can recognise and refactor to compensate.

In further posts I will go into how to apply the same techniques in different styles of functions and start working with classes and objects. For now I hope the takeaway is that with a few changes to writing style we can create more stable code, that still provides the same data in an easy to read format.

To get updates on my new articles, follow me on Twitter @theinfradev

Top comments (1)

Collapse
 
thejoezack profile image
Joe Zack

It's only a couple extra lines, but it adds a lot of value!