Want to Write Beautiful PowerShell Code? Here’s How.

Adam Bertram

Adam Bertram

Read more posts by this author.

Elegant, beautiful code that’s crystal clear to read, easily-manageable and simple to add functionality to. It’s every PowerShell developer’s dream. And it’s possible.

Although perfect code is in the eye of the beholder, there is one concept this PowerShell developer of 12+ years always comes back to; abstraction. Abstraction. It’s a term software developers yawn at and say, “Well, duh!”, but a lot of system administrators and IT professionals leave their head stratching.

In this article, you’re going to learn what abstraction is and how you can apply this coding methodology to any PowerShell code you write. Stay tuned!

Understanding Abstraction (in Code)

In a nutshell, abstraction means writing code that interacts with something just not directly. Abstraction means writing code in “layers” that then refer back to one another eventually building an entire framework around a concept.

By writing code with abstraction in mind helps you loosely-coupled code to easily change over time. Building in an abstraction layer allows you to standardize what you may think is chaotic code. It helps you organize and even build interfaces to ugly code you can’t do anything about.

Building “Portable Code”

How much effort would it take to move your script or module to another computer and actually have it run successfully? How much of the code would have to change? One, ten, 100, lines? If you find yourself getting caught up by this and that environmental dependency, you haven’t leveraged abstraction well enough.

Let’s say you have a line that just saves a file to disk. Is this line somehow wrapped in a function or is it laying bare in a script you call directly? If the latter, you might be doing it wrong.

PS> Add-Content -Path 'somefile.txt' -Value 'XXXXX'

Perhaps the line above is in a script called idontknowbetter.ps1. You invoke this script directly from the PowerShell console when you want to do something.

.\idontknowbetter.ps1

In the current context, it works fine. It adds the expected content to the somefile.txt file. One day, you’re moved to a new team with a new server farm, new PCs and new everything. You bring your script with you because you have a similar task to perform on the new team. Do you have to change the code in the script? Of course you do! somefile.txt is in a different location.

The more changes you must make to your PowerShell code, the more you’re doing it wrong. You should focus on making code resilient across many different contexts and use cases. The best way to do that is through parameters.

Now, what if you had that code in a module wrapped in a function like below? You’re now not calling a script directly, you’re adding to that file indirectly. You’ve “abstracted away” adding content to that specific file. You now don’t call the script directly but you call the New-Stuff function which takes care of it for you.

## ImSmart.psm1

function New-Stuff {
	param(
		[Parameter()]
		[string]$FilePath
	)
	
	## do some stuff here to get the value
	Add-Content -Path $FilePath -Value 'XXXXX'

	## maybe do some stuff down here
}

Why does this context matter? Because when you move to that other team, you don’t have to change any code at all. You simply change a parameter value. You’re separating the “doing” from the “invoking” making code flexible and portable.

Standardization

Another huge benefit of abstraction is standardization. Using the example above, now maybe you need to perform some other related task. The New-Stuff function adds some content to a file. Maybe now you need a similar function to read that file. You decided to create a function called Remove-Stuff. You can already start to see standardization coming into place. The noun is the same!

## ImSmart.psm1

function New-Stuff {
	param(
		[Parameter()]
		[string]$FilePath
	)
	
	## do some stuff here to get the value
	Add-Content -Path $FilePath -Value 'XXXXX'

	## maybe do some stuff down here
}

function Remove-Stuff {
	param(
		[Parameter()]
		[string]$FilePath
	)
	
	## Remove some value here and add it to the file's contents

	Set-Content -Path $FilePath -Value 'XXXXX'

	## maybe do some stuff down here
}

You can now run New-Stuff or Remove-Stuff to do something. These names make perfect sense and you could even expect other functions to be Set-Stuff, Update-Stuff, etc.

You’re standardizing code. You’re creating an interface to the file that follows a predictable pattern.

Building a VM Management Tool

This foofoo concept of abstraction is tough to wrap your head around sometimes, so let’s build a little demo project to hopefully make it sink in.

Let’s say you’ve built some scripts that start, stop, and manage Hyper-V virtual machines (VMs). Your team manages one of the company’s hypervisors which, in this case, is Hyper-V.

Since the VMs are Hyper-V, you decide to use the HyperV PowerShell module. This module is solely focused on only Hyper-V VMs. Perhaps you have some code that looks like the following:

#requires -Module HyperV
[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	[string]$VmName
)

## Use the Start-Vm cmdlet in the HyperV module to start a VM
Start-Vm -Name $VmName

## Do some stuff to the VM here

## Use the Stop-Vm cmdlet in the HyperV module to stop the VM
Stop-Vm -Name $VmName

This script works just fine. You’ve already created a parameter which is a great first start. This parameter allows you to run this script against many different VMs without changing any code inside of the script. Good job!

With this script, you’ve already created one layer of abstraction. Instead of running Start-Vm and Stop-VM directly on the PowerShell console, you decided you needed a script to invoke those commands and run some other code in between. You created it and now, instead of typing Start-VM and Stop-VM at the console, you’re simply running .\Set-MyVM.ps1 or something like that.

You’re not calling the Start and Stop-VM cmdlets directly. You’ve abstracted away the need to do that and instead just interface with the script and not the cmdlets themselves.

Using Abstraction to your Advantage

One day, your boss comes along and tells you your team has now inherited the company’s VMWare cluster. Uh oh! Now you think you need to create an entirely new set of scripts or modules to manage VMWare VMs. You might have to but you could decide to integrate VMware functionality into your current solution.

Notice patterns in code. Instead of building an entirely new codebase, can this new requirement work with the existing solution with minimal effort? What functionality can be shared across both requirements?

A VM is a VM, right? VMware VMs and Hyper-V VMs are similar; they just run on different hypervisors. What are the commonalities between them? You can start and stop both kinds of VMs.

Instead of creating another script, why not integrate them into your current generic Set-MyVM.ps1 script. One way to do that would be to add a parameter called Hypervisor. Once you have that HyperVisor parameter, you can then build the logic needed to determine if your script should run some VMWare code of HyperV code.

Notice that the following code snippet has a required module called VMWare. You’ll need to ensure your script now supports both scenarios. Also, it now has some conditional logic in the form of a switch statement that does some different things based on the type of hypervisor chosen.

#requires -Module HyperV, VmWare
[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	[string]$VmName,

	[Parameter()]
	[ValidateSet('HyperV','VmWare')]
	[string]$Hypervisor
)

switch ($Hypervisor) {
	'HyperV' {
		## Use the Start-Vm cmdlet in the HyperV module to start a VM
		Start-Vm -Name $VmName

		## Do some stuff to the VM here

		## Use the Stop-Vm cmdlet in the HyperV module to stop the VM
		Stop-Vm -Name $VmName
		break
	}
	'VmWare' {
		## Use whatever command the VmWare module has in it to start the VM
		Start-VmWareVm -Name $VmName

		## do stuff to the VmWare VM

		## Use whatever command the VmWare module has in it to stop the VM
		Stop-VmWareVm -Name $VmName
		break
	}
	default {
		"The hypervisor you passed [$_] is not supported"
	}
}

Alternatively and albeit better approach, if possible, you could dynamically figure out what type of VM was passed and then make a decision on the code to run based on the result of that code.

Always try to abstract away concepts as much as possible. Don’t create a parameter if you can come up with the value in code. Don’t force the one who runs the script to run necessary commands if they don’t have to. You might not have abstracted away enough tasks.

See below an example of how you might dynamically figure out the hypervisor. Notice now you leave the decision-making up to the code (which is always better). The script now automatically knows what hypervisor the VM is running on.

#requires -Module HyperV, VmWare
[CmdletBinding()]
param(
	[Parameter(Mandatory)]
	[string]$VmName
)

function Get-Hypervisor {
	## simple helper function to determine the hypervisor
	param(
		[Parameter()]
		[string]$VmName
	)
	
	## Some code here to figure out what type of hypervisor this VM is running on

	## Return either 'HyperV' or 'VmWare'

}

$hypervisor = Get-Hypervisor -VmName $VmName

switch ($Hypervisor) {
	'HyperV' {
		## Use the Start-Vm cmdlet in the HyperV module to start a VM
		Start-Vm -Name $VmName

		## Do some stuff to the VM here

		## Use the Stop-Vm cmdlet in the HyperV module to stop the VM
		Stop-Vm -Name $VmName
		break
	}
	'VmWare' {
		## Use whatever command the VmWare module has in it to start the VM
		Start-VmWareVm -Name $VmName

		## do stuff to the VmWare VM

		## Use whatever command the VmWare module has in it to stop the VM
		Stop-VmWareVm -Name $VmName
		break
	}
	default {
		"The hypervisor you passed [$_] is not supported"
	}
}

You’ve now discovered the commonalities between Hyper-V and VMWare VMs and have built a solution to manage them both! You’ve now created a layer of abstraction that allows you to manage two different VM types with a single script.

Conclusion

The concept of abstraction, standardization, decoupling, and other software development terms is a bit hard to grasp at first. Why? Because these methodologies aren’t black and white. They are higher-level design considerations.

You may be tempted to think you don’t need to concern yourself with this mumbo jumbo and you don’t. You’ll learn the exact same way I did; the hard way. You’ll write thousands of scripts to reinvent the wheel, struggle with making changes and take a whole lot longer than you should.

Keep the concept of abstraction in the back of your head at all times when writing code. Always be recognizing patterns. Always try to standardize as much as possible. Building code like this will take longer at first but you will eventually reap the rewards of beautiful code reuse and built entire solutions rather than scripts.

Subscribe to Adam the Automator

Get the latest posts delivered right to your inbox

Looks like you're offline!