Pretty PowerShell - Formatting PowerShell Scripts for Readability

  • 23 February 2023
  • 6 comments
  • 189 views

Userlevel 7
Badge +20
Alt Text: Title of Post “Pretty PowerShell - Formatting PowerShell Scripts for Readability”

I want to kick this blog post off with a disclaimer, this isn’t about writing ‘perfect’ code, nor about writing the most ‘efficient’ code from a performance standpoint. This blog post is about writing ‘efficient’ code from a readability standpoint.

As a consultant, I find myself jumping between systems for different clients, and unless the tool you’re using is built into the OS or application you’re working with, you either need to make sure you’ve got something portable, or use something standardised.

Because of this, whenever I’m writing PowerShell, or iterating changes, I tend to use PowerShell ISE. It’s a great tool, it allows you to perform script debugging, selective running of some of your PowerShell script, has line numbering, error indicators, and even Intellisense, so it’s easy to rapidly develop iterations of a script.

There are some drawbacks however to PowerShell ISE, and other basic text editors, such as a lack of word/line wrapping. This is where a single line of text, such as a Cmdlet, can be read easily over multiple lines within the editor, even though it executes as a single Cmdlet, which is normally a single line of text.

To work around this, I’ve got a few tricks I’d like to share that I use to format my code for readability. I’ve split these out separately below, and provide examples of how they help, with a dummy collection of them at the end. The beauty of PowerShell and other programming languages, is sometimes there isn’t a right or wrong way, just different ways of trying these things, so I encourage you to share your tips & tricks as well, and if you find a better way to achieve your results, please share also!

 

Multi-Line Cmdlets

 

Some Cmdlets have far more parameters than is reasonable to fit in a single screen, even when working on an ultra-wide display! But if you want to split a single cmdlet over multiple lines, you can just end the sentence with a space ( ) and a backtick (`), then entering a return onto the next line, like below.

Add-VBRBackupRepository `
-Name "Repo001" `
-Description "Backup Repository Example `
-Folder "D:\Backups" `
-Type WinLocal

Consider the alternative that does the exact same thing:

Add-VBRBackupRepository -Name "Repo001" -Description "Backup Repository Example -Folder "D:\Backups" -Type WinLocal

Which one do you think reads better?

 

Commenting with Multi-Line Code

 

You might be used to using the hashtag (#) symbol to comment out code within PowerShell, but this is typically used for end of line comments, and you certainly couldn’t use the backtick and carriage return trick mentioned above with this. But that’s okay, because from PowerShell v2 and above, we have comment blocks. You can write comment blocks anywhere in your code by starting with the following key characters:

<#

And then closing the comments with:

#>

As we can see in my code example below:

Add-VBRBackupRepository <# We use this Cmdlet to add our new backup repository #> `
-Name "Repo001" <# We specify the name of the repository here #> `
-Description "Backup Repository Example <# Here we give it a description #> `
-Folder "D:\Backups" <# This is the path to the directory we want to use #> `
-Type WinLocal <# This is the server type, valid values here are WinLocal, LinuxLocal, CifsShare, ExaGrid, DataDomain, HPStoreOnceIntegration, Quantum, Nfs, Infinidat, Fujitsu, HPStoreOnceCloudBank, Hardened #>

Consider the alternative:

Add-VBRBackupRepository -Name "Repo001" -Description "Backup Repository Example -Folder "D:\Backups" -Type WinLocal #This is the cmdlet we use to add a repository, the -Name Parameter is what we'll call the repository, the -Description Parameter is a description we might choose to add, the -Folder Parameter is the path to the directory we want to use, whilst the -Type parameter is the server type, valid values here are WinLocal, LinuxLocal, CifsShare, ExaGrid, DataDomain, HPStoreOnceIntegration, Quantum, Nfs, Infinidat, Fujitsu, HPStoreOnceCloudBank, Hardened

 

Pipeline Operators

 

Another handy trick is that when you’re trying to manipulate an output from a Cmdlet, you’ll use a pipeline (|) operator. But you don’t need to capture all of this on a single line, if you use a pipe, you can return to another line without the code executing, as it can’t complete without your next command. For example:

Get-VBRBackupRepository -ScaleOut |
Format-List Name, ID, Description, PolicyType, EncryptionEnabled, Extent, UsePerVMBackupFiles

VS:

Get-VBRBackupRepository -ScaleOut | Format-List Name, ID, Description, PolicyType, EncryptionEnabled, Extent, UsePerVMBackupFiles

Admittedly, as the post is automatically wrapping the text, the full benefits aren’t seen, but you certainly see the difference when using PowerShell ISE.

Image displaying code split over two lines
Alt Text: Image showing the first of the two code samples above and how they display within PowerShell ISE
Image showing a single, longer line of text
Alt Text: Image showing the second of the two code samples above and how they display within PowerShell ISE

Whilst I’ve only shown you a single pipe example, you can continue to add new lines with each pipe, making multi-step dependent processes more easily readable.

 

Splatting

 

This one is going to be better suited for scripts and longer-term lifespan code, rather than temporary iterations, but it’s a really good one to know which is why I’ve included it.

Splatting is a way of passing a collection of parameter values to a cmdlet at once. It’s a great way to write clear, and descriptive code. And it overcomes one of the key issues with using backticks, that you don’t need to worry about having an accidental character that gets escaped instead of your carriage return, such as a trailing space after the backtick.

There are two types of splats you can create, an array or a hash table. Microsoft’s documentation gives a full breakdown of the comparisons between these, but the key takeaway is that you can splat with arrays when working with cmdlets that don’t require parameter names, otherwise known as positional parameters, whereas hash tables work with all parameter types such as positional and switch types.

For this example, I’ll be using a hash table. We create a hash table of parameter names & value pairs, and then we call it within our subsequent cmdlet.

$BackupRepositoryArguments =@{
Name = "BackupRepository" # We Specify the Name of the Repository Here
Description ="Backup Repository Description" # We specify the description here
Folder = "F:\FolderPath" # This is the path to the directory we want to use
Type = "WinLocal" # This is the server type, valid values here are WinLocal, LinuxLocal, CIFSShare, ExaGrid, DataDomain, HPStoreOnceIntegration, Quantum, NFS, Infinidat, Fujitsu, HPStoreOnceCloudBank, Hardened
}

Then to use the hash table, we use the @ symbol instead of the $ symbol when referencing it, as below:

Add-VBRBackupRepository @BackupRepositoryArguments

You’ll notice that we can use end-of-line hashtags for commenting as a result of this, and no need for backticks either, making code much easier to read.

Another handy trick with Splatting is the reusability of individual elements within the hash table. For example, there has been a longstanding bug within Veeam Backup & Replication’s Add-VBRBackupRepository cmdlet that you can’t disable the Limit for concurrent backup tasks within the cmdlet, it will default to 4 (Note: I haven’t tested if this exists in the recently released VBR v12). However, you can then call a subsequent Set-VBRBackupRepository cmdlet to disable this. With the hash table the code can look like the below.

$BackupRepositoryArguments =@{
Name = "BackupRepository" # We Specify the Name of the Repository Here
Description ="Backup Repository Description" # We specify the description here
Folder = "F:\FolderPath" # This is the path to the directory we want to use
Type = "WinLocal" # This is the server type, valid values here are WinLocal, LinuxLocal, CifsShare, ExaGrid, DataDomain, HPStoreOnceIntegration, Quantum, Nfs, Infinidat, Fujitsu, HPStoreOnceCloudBank, Hardened
}
Add-VBRBackupRepository @BackupRepositoryArguments
Set-VBRBackupRepository -Repository (Get-VBRRepository -Name $BackupRepositoryArguments.Name) -LimitConcurrentJobs:$false

Finally with Splatting, we can update individual elements within the hash table, for example if we needed to create a new group of backup repositories, with some being Windows and others being Linux, we could pull this information from a CSV for instance, and then change specific parameters as appropriate, whilst reusing most of the code. (Note: You’d be specifying different servers if you were changing type, this code is an example, not exhaustive).

$BackupRepositoryArguments =@{
Name = "BackupRepository" # We Specify the Name of the Repository Here
Description ="Backup Repository Description" # We specify the description here
Folder = "F:\FolderPath" # This is the path to the directory we want to use
Type = "WinLocal" # This is the server type, valid values here are WinLocal, LinuxLocal, CifsShare, ExaGrid, DataDomain, HPStoreOnceIntegration, Quantum, Nfs, Infinidat, Fujitsu, HPStoreOnceCloudBank, Hardened
}

Import-CSV -Path ".\RepositoryList.csv" | ForEach-Object { #We'll read our repository list CSV and run the following code against each line in the CSV file.
$BackupRepositoryArguments.Name = $_.Name #Sets the Name Field to the value within the Name column for the current row of our CSV
$BackupRepositoryArguments.Folder = $_.Folder #Sets the Folder Field to the value within the Folder column for the current row of our CSV
$BackupRepositoryArguments.Type = $_.Type #Sets the Type Field to the value within the Folder column for the current row of our CSV

Add-VBRBackupRepository @BackupRepositoryArguments #Add our backup repository with the arguments either statically defined within the initial @BackupRepositoryArguments hash table or with the updated values where changed
Set-VBRBackupRepository -Repository (Get-VBRRepository -Name $BackupRepositoryArguments.Name) -LimitConcurrentJobs:$false #Workaround for LimitConcurrentJobs bug not working during the Add-VBRBackupRepository cmdlet, could be removed in the future.
}

 

Conclusion & Putting it all together

 

You might be wondering why I bothered to show comment blocks and backticks when I then went against them showing splatting, this is because there are many ways to write code. When writing scripts, the focus tends to be on the automation of a repetitive task, rather than writing high-performance code. Splatting can also be daunting for those not familiar with the concept, so if using a backtick helps someone become more comfortable at writing scripts, then everyone wins.

To close, one final example of how neatly everything can get tidied together

<#
.Purpose
The purpose of this code is to provide a mass-import of backup repositories

.Dependencies
VBR PowerShell modules installed
1x CSV file called 'RepositoryList.csv' that contains three columns, the Name column, the Folder column, and the Type column, stored within the same directory as the PowerShell script
Notes about Parameters:
Name: This is a string that will label the repository name within VBR
Description: This is a string that will populate the Description field within VBR for this repository
Folder: This is a string that must contain a folder path
Type: This is a VBR Repository type, valid values here are:
WinLocal
LinuxLocal
CifsShare
ExaGrid
DataDomain
HPStoreOnceIntegration
Quantum
Nfs
Infinidat
Fujitsu
HPStoreOnceCloudBank
Hardened

#>
$BackupRepositoryArguments =@{ #Creating our hash table
Name = "BackupRepository" # Creating the parameter 'Name' and a default value
Description ="Backup Repository Description" # Creating the parameter 'Description' and a default value
Folder = "F:\FolderPath" # Creating the parameter 'Folder' and a default value
Type = "WinLocal" # Creating the parameter 'Type' and a default value

}
Import-CSV -Path ".\RepositoryList.csv" | #Importing the CSV File
ForEach-Object { #Start iterating through the lines within the CSV until there are no more
<# We'll set the values from the current row of the CSV file #>
$BackupRepositoryArguments.Name = $_.Name
$BackupRepositoryArguments.Folder = $_.FolderPath
$BackupRepositoryArguments.Type = $_.Type
<# Now all the values are set, we'll create our repository within VBR #>
Add-VBRBackupRepository @BackupRepositoryArguments
<#Workaround for LimitConcurrentJobs bug not working during the Add-VBRBackupRepository cmdlet, could be removed in the future#>
Set-VBRBackupRepository -Repository (Get-VBRRepository -Name $BackupRepositoryArguments.Name) -LimitConcurrentJobs:$false
} #Finished this iteration, now onto the next one
Write-Output "Finished"
Image displaying how the code is rendered within PowerShell ISE
Alt Text: Image displaying the code sample above and how it is displayed within PowerShell ISE, consuming 45 lines

Whilst the code is certainly longer, it should be easier to review by anyone new to the script or after some time has elapsed between the writing of the script, and returning to it.

Alternatively without any formatting or tricks from above it would look like the below:

Import-CSV -Path ".\RepositoryList.csv" | ForEach-Object { 
Add-VBRBackupRepository -Name $_.Name -Description "Backup Repository" -Folder $_.FolderPath -Type $_.Type
Set-VBRBackupRepository -Repository (Get-VBRRepository -Name $_.Name) -LimitConcurrentJobs:$false
}
Alt Text: Alt Text: Image displaying the code sample above and how it is displayed within PowerShell ISE, consuming 4 lines

Now, I won’t ignore the fact that this, albeit simpler set of code is undoubtedly smaller. But consider the time it would take for someone else to get up to speed with the code, what questions might they have? Some examples would be:

  • What columns do I need in the CSV file?
  • Why can’t I find the cmdlet, what modules do I need?
  • Why aren’t my values accepted for the Type field?
  • Why are we setting the VBR repository in a second cmdlet when we could do this in the add section?

As the code scales, so do the questions. Or, to end on one of my favourite programming quotes:

When I wrote this code, only God and I knew how it worked. Now, only God knows it!

Unknown

6 comments

Userlevel 7
Badge +14

Great post @MicoolPaul 

 

I do have one thing I need to say about Powershell ISE. It has a mind of its own. Scripts written in ISE may or may not work in a normal powershell session

 

The tips and tricks are all valid and I use a lot of them. 

 

I try to use vscode. I have set a Keyboard shortcut to run selected lines:

  • Press CTRL + K, CTRL +S to open File → Preferences → Keyboard Shortcuts.
  • Search for workbench.action.terminal.runSelectedText in keybindings
  • Press the + icon on the left to open a window with this message “Press desired key combination and then press ENTER” and enter your binding (in my case CTRL + ALT + R)
  • Press ENTER to store your key binding

Or

Select your lines, press F1 and type Run selected text in terminal and press enter.

Userlevel 7
Badge +20

Thanks Maurice, I tend not to have access to VSCode when I’m writing on customer systems, though it is a great tool.

 

One of the key slip-ups that I consciously have to stop myself from doing with PowerShell ISE is interacting with the terminal directly. For good measure once I’ve written my script, if possible, I’ll test it by reopening it, prevents me having done debugging and realised I’m not working from a fresh PowerShell instance.

Userlevel 7
Badge +20

Really great post for sure @MicoolPaul - I read this on your blog before here and it is something I am going to incorporate when using PS.  😎

Userlevel 7
Badge +9

Thanks Maurice, I tend not to have access to VSCode when I’m writing on customer systems, though it is a great tool.

 

One of the key slip-ups that I consciously have to stop myself from doing with PowerShell ISE is interacting with the terminal directly. For good measure once I’ve written my script, if possible, I’ll test it by reopening it, prevents me having done debugging and realised I’m not working from a fresh PowerShell instance.

Great effort! Saw it on Twitter and thought about VSCode or other IDEs! I get the argument now… Cheers!

Userlevel 7
Badge +6

Excellent post…looking forward to more like this!  And great input Maurice!

Userlevel 6
Badge +4

Hello, thanks for sharing.

 

Comment