Appendix-A – Practical PowerShell Office 365: Exchange Online

A. Best Practices

_______________________________

In This Chapter

  • What is a Best Practice?
  • Summary of Best Practices
  • PowerShell Best Practices
  • Conclusion and Further Help

_______________________________

What is a Best Practice?

The use of PowerShell, like any other code in the IT world, requires guidance and best practices to make sure the experience of the coder and end-user are conducive to its use in the real world. As such, a series of best practices has been identified. Now, some of the best practices have already been covered, and will be noted as such, others have been developed over the years that PowerShell has been in use and lastly, some are matters of opinion. For this chapter, the bulk of our time will be spent on the middle topic of the developed best practices. At the end of the chapter we'll also explore some third party options that will help us code better with code analysis.

Summary of Best Practices

  • Comment block at the top of the script
  • Comments in script for documenting operation
  • Useful comments
  • Variable naming
  • Variable block
  • Matching variables to parameters
  • Preference variables
  • Verb-Noun-Functions and scripts
  • Single task function
  • Signing your code
  • Filter vs. Where
  • Error handling
  • Write-Output / Write-Verbose
  • # Requires
  • Set-StrictMode
  • Capitalization
  • Using full command names
  • Cmdlet binding
  • Script structure
  • Quotes
  • Running applications

PowerShell Best Practices

Commenting

When it comes to PowerShell scripting, comments can be extremely useful when documenting a script, but also for troubleshooting a script’s performance. Commenting is especially important for scripts that are meant for public disbursement. If someone else using your script runs into a problem, it would be ideal to either have copious commenting or to have some sort of documentation for them to reference. Below are samples we have used in the real world.

Example 1

Notice that the comment below lets us know that we are loading more modules:

# Load PowerShell Modules

Import-Module MSOnline

Import-Module ActiveDirectory

Example 2

Get users with retention policies assigned:

# Gets all servers that have a mounted database

$Mailboxes = (Get-Mailbox ).RetentionPolicy | Where {$_ -ne $Null}

Example 3

##################################

# Global Definitions #

##################################

$DistributionGroup = "IT Department"

$RetentionPolicy = "AllItems"

$IssueWarning = "20GB"

** Note ** A more detailed description of PowerShell commenting is available on page 372 of this book.

Useful Comments

Another Best Practice for comments is simply to make sure to put useful information into the comment sections. Use comments to denote breaks. Use them to describe what a section does. Use them to describe variables purposes or maybe helpful troubleshooting information.

Variable Naming

Most of us are guilty of this one. Have you ever needed to create a quick script and in that script, for example, you needed a numerical counter so you could keep track of something for later? Well, I can almost guess that you used '$A++' or '$N++' to make your counters. This is a best practice that most of us need to break. The best way to handle such variables is to provide a meaningful name. If for example you need a list of all mailboxes, make the variable name '$Mailboxes'. If you need all users, make the variable $Users and so on.

Examples – Following the best practice

$GivenNames = (Get-User -Filter *).FirstName

$Counter++

$Mailboxes = Get-Mailbox

Examples – Not following best practice

$N = (Get-User -Filter *).FirstName

$I++

$MBX= Get-Mailbox

Variable Block

In the spirit of previous best practices, another best practice concerning variables is creating a variable block. A variable block is an area of the script (at the top) that defines all the variables. It's usually started with a comment like so:

# Variable definition

Or

################ VARIABLES ################

Then the variables to be used in the script are defined below that.

Example – from a working script

################################

# Global Variable Definitions #

################################

$NewProxy = $Null

$Domain = 'domain.com'

$TestUserUPN = 'licensing@domain*'

# Proxy Addresses

$Proxy = (Get-User -Filter * -Property * | Where-Object {$_.UserPrincipalName -Like $UserUPN}).ProxyAddresses

Matching Variables to Parameters

Another naming convention that should be followed, is to name variables that match a parameter or value from a query. An example of this would be:

$DisplayName = (Get-Mailbox).DisplayName

The reason for this is less confusion. This isn't necessarily a hard and fast rule. A variation of this is to name the variable after the content you intend to store. Using the above cmdlet as an example:

$RetentionPolicy = (Get-Mailbox).RetentionPolicy

OR

$Users = Get-MSOLUser

The intent is to keep the name of the variable as close as possible to the name of the parameter or value. This gives PowerShell variable names a purpose.

** Note ** One caveat to variable names, there are a set of predefined or 'Automatic Variables' that PowerShell has and you need to be aware of. These variables names are documented by Microsoft here:

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_automatic_variables?view=powershell-6

Preference Variables

What is a Preference Variable in PowerShell? Preference Variables determine certain behaviors in PowerShell for how cmdlets should process certain conditions. These conditions include Errors, Debugging, WhatIf, Warnings, etc. To see which of these variables are available in PowerShell we can run this:

Get-Variable *preference

These are your default values for each Preference Variable in PowerShell. The best practice is not to change these settings globally because this configuration is what would be called expected behavior. If settings need to be changed, these should be changed on a case by case basis:

Get-Mailbox Damian –ErrorAction STOP

Not

$ErrorActionPreference = “STOP”

If we were to run this line we would change how all cmdlets would respond to errors when run. This makes troubleshooting more difficult and changes the expected behavior in PowerShell.

Naming Conventions, this time for Functions and Scripts

This is one thing we did not cover in the book in-depth or even at all. Earlier in the book we talked about naming conventions for variables in terms of capitalization and in this chapter we talked about using normal language instead of abbreviations for variables. This same concept can be extended to include functions and scripts. For these the best practice is to follow a similar naming convention as cmdlets in PowerShell. This would mean that function and script names should follow a 'Verb-Noun' naming format. Some examples are listed below:

Functions

Function Add-Licenses {}

Function Change-Quotas {}

Function Send-EmailMessages {}

Script Names

Set-RetentionPolicies.ps1

Test-ActivesyncOffice365.ps1

Set-MailboxSettings.ps1

As you can see from the above examples, we tried to be descriptive of what the function or script does. The same was done with scripts.

Singular Task Functions

This is an easy one. Functions can run all sorts of PowerShell cmdlets inside of them. However, the simpler more focused they are the better. As a best practice, functions should perform a single task like looking up a hotfix or installing a particular program or maybe making a registry change. The reason for this is to keep things simple and repeatable. That is the very definition of a function. Functions should be stackable in the sense that they can be called and used to build tasks (like LEGOs to build a house). We want to make sure the functions we construct are not overly complex or perform too many actions. This could complicate troubleshooting.

An example of this would be:

Example

This function will check for a certain module to be present which could affect a portion of the script:

Function Mail-ManagerDisabled ($GroupSmtpAddress) {

$Manager = ((Get-DistributionGroup $GroupSmtpAddress).ManagedBy)

$Manager | Foreach-Object {

$EmailAddresses = (Get-Mailbox $_).EmailAddresses

$EmailAddresses | Foreach-Object {

If ($_ -CMatch "SMTP*") {

$SMTPAddress = $_.Substring(5)

$SMTP += @($SMTPAddress)

}

}

}

$DLName = (Set-DistributionGroup $GroupSmtpAddress).DisplayName

[String] $Body = “<strong>NOTIFICATION</strong><BR><BR>As a part of regular maintenance, IT has decided to monitor the usage of Distribution # Lists.<BR><BR>This email is a notification that an Email Distribution Group that you manage has been inactive for over 12 months. This Distribution group has been deleted.<BR><BR>Please send an email to $AdminAddress if you have any questions. Thanks for your assistance with this matter.”

Foreach ($Line in $Smtp) {

$MessageParameters = @{

Subject = “Distribution Group Manager Alert - Removed Distribution Group - $DLName”

Body = $Body

From = $From

To = $Line

SmtpServer = $SmtpServer

}

Send-MailMessage @MessageParameters –BodyAsHtml

}

} # End Function Mail-ManagerDisabled

Signing Your Code

Signing your code will help with two things. If downloaded by someone else, it ensures that the code has not been modified by anyone after it was signed, and if there is a strict PowerShell policy in place (which there is by default) the script can be run without changing this security feature.

As this is already covered, you can find more information on Code Signing on page 42 of this book.

Filter vs. Where

When it comes to manipulating data results, the Filter parameter and the Where operation are two different approaches to narrowing data to search from. With Filter the results are pre-filtered by a Domain Control or other system outside of PowerShell. With Where the results are all returned to PowerShell and then filtered. Because of this, the speed performance of the two results can vary greatly.

This is covered on page 33 of this book.

Error Handling

When first starting out to script, it isn't uncommon for there to be a reliance on '-ErrorAction SilentlyContinue' for pseudo error handling in a script. This allows for a one-liner or cmdlet to continue even if errors crop up that would otherwise end its operation. The problem with using this technique is that it can hide all errors. Errors that could be different from the original one that broke a script from running will now be hidden. A better way to handle this is to use Try and Catch. These two together can perform effective error handling where a cmdlet succeeds in the 'Try' part of the code block.

Try and Catch was covered earlier in the book and you can read more about this on page 376 of this book.

Write-Output / Write-Verbose

There are a few ways to output information while a script is running – Write-Host, Write-Verbose and Write-Output. Write-Host will immediately display the information to the PowerShell window as the script or line is run. For example, if we were to get a list of all Mail Contacts in Exchange and then display them in a certain way, we can use Write-Host to visually represent data:

$Contacts = Get-MailContact

Foreach ($Contact in $Contacts) {

$Name = $Contact.Name

Write-Host “$Name is a mail contact.”

}

Using Write-Host is not recommended because a lot of scripts are run for automation and thus any output to a PowerShell window may be useless. A better option is to use Write-Verbose or Write-Output. Both of these provide a completely different option for your PowerShell script.

** Note ** Good reading on Write-Host can be found here -https://blogs.technet.microsoft.com/heyscriptingguy/2014/03/30/understanding-streams-redirection-and-write-host-in-powershell/.

Write-Verbose

Write-Verbose is an additional tool for troubleshooting the operation of a script. This cmdlet can be placed throughout the script at key points to help document.

Example

In this example, we use Write-Verbose to display information being gathered by a script:

Write-Verbose "The $DisplayName mailbox has had the $Policy retention policy applied."

Write-Verbose "A connection to Exchange Online could not be made."

Write-Verbose "No mailboxes were found."

If the script is run without the –Verbose switch, no output is displayed. However, if it experiences issues, we can use the –Verbose switch to trigger the Write-Verbose:

Write-Output

This cmdlet can be used in two instances, it can be used to display the contents found from the running of a cmdlet/one-liner like so:

Get-UnifiedGroup | Write-Output

This is a bit redundant as the same output can be produced with:

Get-UnifiedGroup

It can also be used to produce the output in a variable:

$Mailboxes = Get-Mailbox

Write-Output $Mailboxes

However, the same process can be done without the Get-Output cmdlet:

$Mailboxes = Get-Mailbox

$Mailboxes

Stick with Write-Verbose for most scripts and only use Write-Host if a visual answer / question or menu is needed.

'# Requires'

PowerShell scripts can be written to be standalone or they can be written to utilize other modules or components. If the latter is the case, then the use of '# Requires' should be in the script. Adding this to a script will prevent a script from running if the requirement is missing. This is important because without this a script that references to a module or particular component that is not available will fail. The PowerShell window will be covered with red text as the script fails to run. The most common usage that we've seen is when a certain level of PowerShell is needed. PowerShell's version level could determine important items like what cmdlets are available or what switch / parameters would be available to be run by the script.

What can we require? How do we figure that out? The easiest way is to fire up your favorite browser / search engine and look for the following:

PowerShell #Requires

These search terms provide us with these results on the next page:

If we explore the resultant page, we find some more information on how to use the ‘#Requires’ feature:

Example 1

For our first example, we’ll test requiring a certain version of PowerShell. The test will be run on a Windows 2008 R2 server. What version of PowerShell runs on Windows 2008 R2 by default?

$PSVersionTable reveals the following:

The PowerShell version is 3.0. As such, if we set the Requires for version for 4.0 like this:

#Requires –Version 4.0

Then when the script is run, it will fail:

The same script run on a Windows 2012 R2 server would not have any issue at all. It runs because if we check the $PSVersionTable, we see that Windows 2012 R2 is at version 4.0:

Example 2

From the Syntax provided from the MSDN page, we see that we can also require certain modules before running like this:

#Requires –Modules MSOnline

If the MS Online module is not loaded, the ‘Requires’ will force a module load. If the module cannot be loaded, a terminating error occurs and the script will exit.

PowerShell also provides a Get-Help for the #Requires feature. ‘Get-Help Requires’ will provide more detail on how to use ‘#Requires’.

Set-StrictMode -Version Latest

This is one that we've started adding to our scripts now. What this does is it enforces certain best practices in your PowerShell coding and forces the code to be 'Correct'. Think of this as a code check prior to execution of the script. One item that is checked is variable definition. Variables that are used in a script will need to be defined at the top of the script.

How can we configure this PowerShell cmdlet, let’s review the Get-Help for the cmdlet to see:

Get-Help Set-StrictMode –Full

The valid values are "1.0", "2.0", and "Latest". The following list shows the effect of each value.

1.0

-- Prohibits references to uninitialized variables, except for uninitialized variables in strings.

2.0

-- Prohibits references to uninitialized variables (including uninitialized variables in strings).

-- Prohibits references to non-existent properties of an object.

-- Prohibits function calls that use the syntax for calling methods.

-- Prohibits a variable without a name (${}).

Latest:

--Selects the latest (most strict) version available. Use this value to assure that scripts use the strictest

available version, even when new versions are added to Windows PowerShell.

Version 1 - Example:

In this example we’ll use an undefined variable (one without a value):

Set-StrictMode -Version 1.0

If($A > 2) {

Write-Host "Large Number"

}

Now if we define a variable correctly and still use the strict mode, no error will occur:

Set-StrictMode -Version 1.0

$Count = (Get-command *Mailbox*).Count

If($Count > 2) {

Write-Host "Large Number"

}

Version 2 - Example

In this example we review one of the Version 2 best practices. Let’s try out the restriction on calling nonexistent properties.

No sub-properties to call and no version defined:

$Mailboxes = "Dave"

$Mailboxes.DisplayName

No results or errors will show with that code.

Now we try the same code with StrictMode Version 2:

Set-StrictMode -Version 2.0

$Mailboxes = "Dave"

$Mailboxes.DisplayName

Best practice Followed:

Set-StrictMode -Version 2.0

$Mailboxes = Get-Mailbox

$Mailboxes.DisplayName

This last example worked because the variable $Mailboxes has a sub-property called DisplayName and when called it displays its value.

The last value for –Version is a bit deceptive. ‘Latest’ is the same as ‘2.0’ for now. As such the best practice is to use ‘Latest’ so that if something is added later on, you are not stuck with old code looking for ‘2.0’ restrictions and not something newer that was added.

Capitalization

Capitalization is just another best practice that has more to do with readability and formatting than a strictly PowerShell best practice.

** Note ** This is already covered in the book on page 26. See that section for more detail on this best practice.

Using full command names

PowerShell contains quite a few aliases (or shortcuts) that are basically shortcuts to common cmdlets like 'Foreach-Object', 'Where-Object' and 'Write-Output'. Aliases are typically used for a couple reasons:

  • It's quicker to type the shortened version:

    Example

    '%' rather than typing the equivalent 'Foreach-Object'

  • Simplifies the coding, can point aliases towards commands and functions.

Why not use aliases? Aliases should not be used when coding a script for the public consumption or trying to explain PowerShell coding to another person. Make sure to put full commands in so that there will be no confusion as to why something was used. For learning purposes, using the Verb-Noun format of PowerShell is more conducive then trying to get someone to learn the multitude of aliases that are present in PowerShell. For example, Exchange Online has 158 aliases.

How do we find that out?

(Get-Alias).Count

In addition to this, when building a new script, it would be better to have the correct syntax / commands so that if you need help troubleshooting something there won't be any head-scratching on trying to decipher aliases. Thus having the full commands is better for you and someone helping you.

Details of PowerShell aliases are covered on page 410 of this book.

Cmdlet Binding

Cmdlet binding is used to add additional functionality to functions within a PowerShell script. Using cmdlet binding also allows for the use of Write-Verbose. Write-Verbose can be useful for troubleshooting a script or providing more information on a particular section of a script.

How do we use this? In this example we’ll see if we can get the Write-Verbose cmdlet to work:

Function Test-AdvancedFeatures {

[CmdletBinding()]Param()

Try {

Import-Module MSOnline -ErrorAction STOP

} Catch {

$Failed = $True

Write-Verbose "Cannot load the MSOnline module."

}

}

Test-AdvancedFeatures -Verbose

Without the verbose switch, no feedback is given:

However, with the verbose switch:

In addition to this, Cmdlet Binding can also add functionality like –WhatIf or –Confirm, or even ErrorVariable and ErrorAction to a script or function. The use of this option expands options that are available for use with PowerShell functions to the point of them operating like cmdlets specifically with the switch options.

Further reading on Cmdlet Binding - https://msdn.microsoft.com/en-us/powershell/reference/5.1/microsoft.powershell.core/about/about_functions_cmdletbindingattribute

Script Structure

For ease of use, readability, and general flow, a good script structure is generally recommended. In general a script should follow something like this:

Comment block – script description, parameter definitions, versioning and more

Global variable definitions – define arrays and other variables that may need to be pre-populated like dates

Functions – there should be a section of the script near the top that defines the functions that will be used in the script (not required)

Script body – where the script starts to run and use the variables and functions that were predefined in order to accomplish some task

Example Script - Structure

Quotes

Quoting in a PowerShell script may not seem important, but it can determine if a script runs correctly. The use of single quotes (') versus double quotes (") makes a vast difference in scenarios with variables that may need to be called from inside the quotes. This topic was covered earlier on page 40 of this book.

Running Applications

This is the easiest of the best practices in PowerShell. When calling an executable from PowerShell, make sure to use the ‘.exe’ file extension. If you were to call an application without the extension and the application name happens to match a PowerShell alias, then the alias is called and not the executable. One example of this would be ‘sc’.

Get-Alias sc

Whereas the sc.exe executable file would need to be called directly with ‘sc.exe’ instead.

Conclusion and Further Help

When it comes to PowerShell, best practices are defined in order to guide us into producing better code for ourselves and others that we are coding for. Whether it means more consistent capitalization, easy to remember variables or signing the script code, the purpose is the same. The list in this chapter is in no way a comprehensive list of all best practices for PowerShell. Make sure to follow the rules suggested, but also use external information to validate. In addition to the list provided, there are vendors besides Microsoft that produces PowerShell tools to help keep your script quality. One of these products is called ‘PsScriptAnalyzer’. This tool is available on Github:

https://github.com/PowerShell/PSScriptAnalyzer

The ISE from Microsoft, the one newest versions, come with the Script Analyzer built in.

Once you have a script opened, you can run the tool with this:

On the right will be a list of suggested fixes:

You might find the analysis to be a bit over picky, but if you would like your script to be more stream-lined and accurate, then this is the way to go.