PowerShell – Best Practices – Part 2

PowerShell is a very flexible language with many ways to solve a problem, and many ways to write the same code. To ensure readability, consistency, and reuseability, it’s very important to always adhere to community accepted best practices. Not everyone will agree with everything, but the majority will agree that these practices will improve your code by leaps and bounds, and make it easier for other people to both understand and use. We will split up this into a series of blog post, so we have room for showing examples along the way.

Note that these practices only apply when authoring scripts. What happens in the command line, stays in the command line.. So without further ado, here’s some best practices you should follow when writing PowerShell code.

Use proper indention

You should always indent your code where necessary. An indention should consist of 4 spaces (TAB), and goes long ways to make your code much easier to read. It also denotes to which construct each line belong.

# Bad practice:
function Get-MultiplicationTable {
param(
[int]$Start,
[int]$End
)
$Numbers = $Start..$End
foreach ($NumberX in $Numbers) {
foreach ($NumberY in $Numbers) {
[PSCustomObject]@{
X = $NumberX
Y = $NumberY
Multiplied = $NumberX * $NumberY
}
}
}
}

# Good practice:
function Get-MultiplicationTable {
    param(
        [int]$Start,
        [int]$End
    )
    $Numbers = $Start..$End
    foreach ($NumberX in $Numbers) {
        foreach ($NumberY in $Numbers) {
            [PSCustomObject]@{
                X = $NumberX
                Y = $NumberY
                Multiplied = $NumberX * $NumberY
            }
        }
    }
}

Avoid unecessary commenting

PowerShell is a very verbal language, that closely resembles english. When using full parameter and function names, it’s by design very self-documenting what a piece of code does. Excessive commenting is unnessecary, and will make your code harder to follow.

# Bad practice:

# The variable $BannedProcesses contains a list
# of banned processes that should be stopped
$BannedProcesses = @('spoolsv', 'firefox')

# The variable $Processes contains information about the running processes
$Processes = Get-Process

# Start a foreach loop, iterating over all processes
foreach ($Process in $Processes){
    # Check if the process is banned
    if ($Process.ProcessName -in $BannedProcesses){
        # If the process is banned, stop it
        Stop-Process -Name $Process.ProcessName
    }
}
# End foreach loop on $Processes

# Best practice:

$BannedProcesses = @('spoolsv', 'firefox')
$Processes = Get-Process

foreach ($Process in $Processes){
    if ($Process.ProcessName -in $BannedProcesses){
        Stop-Process -Name $Process.ProcessName
    }
}

Write comment-based help

If you are writing advanced functions, you should be documenting the functions through comment-based help. I have written about this in another post on this blog:
PowerShell – Build Advanced Functions

Use #region

In order to segment large chunks of code, use the #region functionality. This allows you to document where something is starting and ending, and to fold away the code when you don’t need it. This is a must in long scripts.

#region expanded
#region collapsed

Use CIM cmdlets instead of WMI

WMI cmdlets are deprecated in favor of the CIM cmdlets. For most WMI use-cases, a similiar CIM method exists.

# Bad practice:
Get-WmiObject -ClassName Win32_Volume
Get-WmiObject -ClassName Win32_Computersystem

# Best practice:
Get-CimInstance -ClassName Win32_Volume
Get-CimInstance -ClassName Win32_Computersystem

Use #requires

Very few beginners actually know about this seemingly simple functionality in PowerShell, and what it is capable of. Stick it to the top of your script to define requirements that must be compliant before running your script. You can add one, or more of these statements, separated by a new line.

Version

Specify the minimum PowerShell version to run the script.

#Requires -Version 5.1
Run as administrator

Require that the PowerShell session that invokes your script, must be run with local admin privileges.

#Requires -RunAsAdministrator
Modules

Specify modules that your script requires to be installed and imported. If not already imported in the session, this will do that for you.

#Requires -Modules ActiveDirectory, DNSServer
Assembly

Specify required DLL’s to load:

#Requires -Assembly path\to\my.dll

Use splatting

In PowerShell version 3, they introduced this great functionality called splatting. It allows us to defied a series of parameters as a hashtable, instead of writing endlessly long lines of code. Think about how many parameters New-ADUser takes for example. It has always been posible to separate parameters on a new line by using back-ticks ( ` ) but this is frowned upon by the community, stating that it can be hard to spot, and if you place a space after the back-tick, you break your code.

To use splatting define a hashtable with properties, then use variable on the cmdlets, but with the ‘@’ sign instead of ‘$’.

# Bad practice:
New-ADUser -Name 'TestName' -DisplayName ' Test Name' -EmailAddress 'test.name@testdomain.test' -UserPrincipalName 'test.name@testdomain.test' -Path "OU=Users,DC=testdomain,DC=test" -Enabled $true -ChangePasswordAtLogon $true -AccountPassword (Read-Host -Prompt 'Password' -AsSecureString)

# Still bad practice:
New-ADUser -Name 'TestName' `
           -DisplayName 'Test Name' `
           -EmailAddress 'test.name@testdomain.test' `
           -UserPrincipalName 'test.name@testdomain.test' `
           -Path "OU=Users,DC=testdomain,DC=test" `
           -Enabled $true `
           -ChangePasswordAtLogon $true `
           -AccountPassword (Read-Host -Prompt 'Password' -AsSecureString)

# Best practice:
$Properties = @{
    Name = 'TestName'
    DisplayName = 'Test Name'
    EmailAddress = 'test.name@testdomain.test'
    UserPrincipalName = 'test.name@testdomain.test'
    Path = "OU=Users,DC=testdomain,DC=test"
    Enabled = $true
    ChangePasswordAtLogon = $true
    AccountPassword = (Read-Host -Prompt 'Password' -AsSecureString)
}
New-ADUser @Properties

Conclusion

In this post you have been introduced to some of the most important best practices in order to make your code consitent, readable, and reusable. In the next blog in this series, we’ll take a look at more PowerShell best practices you should be following.

Leave a Reply