Skip to Content
Style GuidesPowerShell

PowerShell Style Guide

Note: This guideline does not cover Cmdlet development (PowerShell Cmdlets written in C#). Cmdlet Guidelines, tools, and how-to’s will be covered in a separate documentation.

TODO

  • Add Pester Section and Recommendations.
  • Add/Change Comment Based Help recommendations to PlatyPS.
  • Add ScriptAnalyzer Section and Recommendations.
  • Add Module Distribution Section and Recommendations (Internal PSGallery/Support for Install-Module/How to deploy my module to repository).
  • Add recommendations regarding data files (psd1) for storing large sets of hashtables.
  • Add recommendations regarding cyclomatic complexity. (https://github.com/MathieuBuisson/PSCodeHealth )

Quick Checklist

  • Should express the intent of the programmer
  • Should have variable names that explain their purpose.
  • Should have complex code split up in smaller, internal functions.
  • Should have correct naming conventions.
  • Should have correct spaces in conditional statements and loops.
  • Should use correct number of tabs (4 spaces).
  • Should follow the rules of simple design
  • Should follow the principles, patterns, and practices
  • Should only have comments if necessary
  • Should avoid using aliases in code.
  • Should include test cases.
  • Should pass ScriptAnalyzer tests.

Recommendations Checklist

ConceptRecommendation
VariablesStrongly type your variable whenever you create a new variable since PowerShell may not automatically pick the best type for the scenario.
Conditional StatementsUse a space between statement and the parenthesis. Place the first brace on the same line. The statement is preceded by one tab.
Looping StatementsUse a space between statement and the parenthesis. Place the first brace on the same line. The statement is preceded by one tab.
Functions
  • External function names follow the Verb-Noun standard.
  • The verb should be an approved verb (you can use the Get-Verb cmdlet to list all approved verbs).
  • Use a specific noun for a cmdlets name. The noun should be in singular.
  • Use Pascal Case for function names. In other words, capitalize the first letter of verb and all terms used in the noun. For example “Get-TSOperatingSystem”.
  • Add a company, or product specific prefix to all nouns. this is to avoid collisions with other functions.
  • Try to keep the code as short as possible. repeatable code should be placed in a separate helper function.
  • Functions should perform single actions, not multiple actions
Param Statement
  • The parameter names are standardized so that the user can quickly determine what a particular parameter means.
  • Use singular names for parameters where the value is a single element. This includes parameters that take an array or a list as input.
  • Plural names could be used only if the value is always a multi-element value.
  • Use Pascal Case for parameter names.
  • Strongly type the parameter.
Advanced Parameters
  • Add the mandatory attribute on all required parameters.
  • Use validation if you except a specific type of input, such as a range of numbers or specific values.
  • Add pipeline support to your functions.
Comment Based HelpAdd comment based help to your functions. This helps the user to understand how a function works and how it is expected to be used. Include Parameter descriptions and Examples.
Output
  • Output objects of the same type.
  • Do not output multiple lines of text, use Verbose or Debug to output text.
Error Handling
  • Avoid using empty catch blocks. If an error occurs the user should be aware of it.
  • When using try-catch with a PowerShell cmdlet, add ErrorAction Stop.
  • Avoid using ErrorAction SilentlyContinue.
Module Manifests
  • Add manifests to your modules.
  • Use the manifest capabilities in your manifest.

General coding guidelines

These guidelines demonstrate how we write PowerShell code in the team. The purpose of these guidelines is to have code consistency in our code. This simplifies reviews, updates, and sets a standard for how we work as a team.

Recommended developer environments:

PowerShell Module Development

  • Visual Studio Code
ModuleDescription
PlatyPSWrite External Help files in Markdown
PSScriptAnalyzerEvaluates a script or module based on selected best practice rules
PesterTest and Mock Framework for PowerShell

Table of capitalization

IdentifierCaseExample
Language keywordLowertry, catch, foreach, switch, if
Process block keywordLowerbegin, process, end, dynamicparameter
Comment help keywordUpper.SYNOPSIS, .EXAMPLE
ModulePascalTruesec.Service
ClassPascalNew-Object System.Text.StringBuilder
ExceptionPascal[System.Management.Automation.CommandNotFoundException]
Global variablePascal$Global:someVariable
Script variablePascal$Script:someVariable
Environment variablesLower$env:VariableName
Local variableCamel$camelCase, $args, $this
Public functionPascalfunction Get-TSService
Private functionPascalfunction GetHashCode
Function parametersPascal$Parameter
ConstantPascal$PascalCase
CmdLetPascalGet-Process
Type aliasLower[string], [int]

Variables

Windows PowerShell, like most other scripting languages, stores values in variables Variables are represented by single-word text strings that begin with the dollar sign ($). Windows PowerShell supports four types of variables: user-created, automatic, preference and environment variables. The example below demonstrates how to store a string in a variable:

PS > [string]$givenName = "Johan"

The variable name should be written in camelCase and it should be strongly typed. You should also use a variable name that describes the variables value. Using this approach allows you to use less comments in your scripts and makes it easier for other developers to read.

We recommend that you strongly type your variable whenever you create a new variable since PowerShell may not automatically pick the best type for the scenario.

Data Types

Windows PowerShell supports the types available in the .NET framework. The table below shows some of the most common types used. PowerShell also supports type name aliases for common types.

Type alias.NET typeDescriptionExample
[string]System.StringString of unicode characters[string]$givenName = “Johan”
[int]System.Int3232-bit integer[int]$number = 12
[int32]System.Int3232-bit integer[int32]$number = 12
[long]System.Int6464-bit integer[long]$number = 1200000
[int64]System.Int6464-bit integer[int64]$number = 1200000
[char]System.CharUnicode character[char]$character = 34
[bool]System.BooleanTrue or false value[bool]$isAvailable = $false
[byte]System.Byte8-bit integer[byte]$number = 255
[decimal]System.DecimalDecimal number[decimal]$number = 12.44
[double]System.DoubleDouble precision decimal number[double]$number = 12.44
[float]System.SingleSingle-precision floating number[float]$number = 12.44
[single]System.SingleSingle-precision floating number[single]$number = 12.44
[datetime]System.DateTimeDate and time[datetime]$dateTime = “1/1/2010”
[array]System.ArrayAn array[array]$numbers = 1,2,3
[hashtable]System.Collections.HashtableHash table[hashtable]$user = @{GivenName=“Johan”}
[xml]System.Xml.XmlDocumentXML document[xml]$user = “<name>Johan</name>

Conditional Statements

if (<expr>) { <statements> } if (<expr>) { <statements> } else { <statements> } if (<expr>) { <statements> } elseif (<expr>) { <statements> } else { <statements> }

The if/elseif/else statement allows us to execute a block of code if a specified condition is met. However, the statement can also execute a block of code if a condition is not met depending on the operator that you use in the expression. The example below demonstrates a simple if conditional statement.

[int]$number = 10 if ($number -le 10) { "Number is less than or equal to 10" }

We recommend using a space between if and the parenthesis. We also recommend placing the first brace on the same line. The statement is preceded by one tab.

Looping Statements

while (<expr>) { <statements> } do { <statements> } while (<expr>) do { <statements> } until (<expr>) for (<expr>; <expr>; <expr>) { <statements> } foreach ($var in <pipeline>) { <statements> }

The while loop is a language construct used to run a command block as long as a condition evaluates true. Here is an example of a simple while loop.

[int]$number = 2 while ($number -ne 3) { $number++ } PS > $number 3

The do-while loop always executes at least once before checking the condition.

[int]$number = 2 do { $number++ } while ($number -le 2) PS > $number 3

The do-until loop is identical to the do-while loop except that the test is inverted and the statement will loop until the condition is true instead of while it is true.

[int]$number = 2 do { $number++ } until ($number -ge 2) PS > $number 3

The for loop is a construct used to run commands in a statement block for as long as the specified condition evaluates to true. The for loop is often used to iterate through an array or other type of collection and run a set of commands against each of its elements. Here is an example of a simple for loop.

for ([int]$i = 0; $i -le 10; $i++) { $i }

The foreach loop is a construct used to iterate through a series of values in a collection of items. A block of code contained within braces is used to execute a statement for each item in the collection. Here is an example of a basic foreach loop:

[int[]]$numbers = 1..10 foreach ($number in $numbers) { $number }

We recommend using a space between the loop and the parenthesis. We also recommend placing the first brace on the same line. The statement is preceded by one tab.

Functions

Functions are used in most programming and scripting languages. A function is a named block of code that can be referred to from within Windows PowerShell. When a function’s name is called, the list of statements contained in the function are executed. A function may accept input in the form of arguments, the values of which can then be used by the code inside the function. The output from a function can be stored in a variable, passed to another function, passed to a cmdlet or written to one of the output streams. A function should perform a single action, not multiple actions. A function is declared with the keyword function and the associated code is placed within a scriptblock. Here is an example on a basic function:

function Get-TSOperatingSystem { Get-CimInstance -ClassName Win32_OperatingSystem }

We recommend that:

  • function names follow the Verb-Noun standard.
  • The verb should be an approved verb (you can use the Get-Verb cmdlet to list all approved verbs).
  • Use a specific noun for a cmdlets name. The noun should be in singular.
  • Use Pascal Case for function names. In other words, capitalize the first letter of verb and all terms used in the noun. For example “Get-TSOperatingSystem”.
  • Add a company, or product specific prefix to all nouns. this is to avoid collisions with other functions. The function above uses “TS” as a prefix.
  • Try to keep the code as short as possible. repeatable code should be placed in a separate helper function.
  • Functions should perform single actions, not multiple actions

Internal Functions

Internal functions are functions that are not exposed to the end-user. To distinguish the internal functions from external, they do not have to follow the Verb-Noun structure. Example:

function GetSerialNumber { Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty SerialNumber }

Parameters

The param statement is used to add parameters to scripts. The param statement shall be the first executed line of code in a script except for comments or comment-based help. The example below demonstrates how to add a parameter to a function.

function Get-TSOperatingSystem { param ( [string]$ComputerName ) Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName }

We recommend that:

  • The parameter names are standardized so that the user can quickly determine what a particular parameter means.
  • Use singular names for parameters where the value is a single element. This includes parameters that take an array or a list as input.
  • Plural names could be used only if the value is always a multi-element value.
  • Use Pascal Case for parameter names.
  • Strongly type the parameter.

Cmdletbinding

When you write functions, you can add the CmdletBinding attribute so that Windows PowerShell will bind the parameters of the function in the same way that it binds the parameters of compiled cmdlets. When this attribute is declared, Windows PowerShell also sets the $psCmdlet automatic variable. The $psCmdlet variable contains additional methods and properties that can be used within the function to add extra functionality. The example below demonstrates how to include a cmdletbinding and add WhatIf functionality.

function Get-TSOperatingSystem { [cmdletbinding(SupportsShouldProcess = $true)] param ( [string]$ComputerName ) if ($psCmdlet.ShouldProcess("Get-TSOperatingSystem", $ComputerName)) { Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName } }

Advanced parameters

The whole idea of advanced parameters is to make your functions more CmdLet alike, containing help information, accepting values from pipelines and controlling the user input. If a condition is not met a controlled error is returned and the function code is not executed. This is a good practise to incorporate in your own functions to avoid unnecessary errors.

Mandatory

You should use the Mandatory attribute to control whether the parameter is required. If this attribute is not specified, the parameter is optional

function Get-TSOperatingSystem { [cmdletbinding(SupportsShouldProcess = $true)] param ( [parameter(Mandatory = $true)] [string]$ComputerName ) if ($psCmdlet.ShouldProcess("Get-TSOperatingSystem", $ComputerName)) { Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName } }

We recommend adding the mandatory attribute on all required parameters

Validating Input

You can validate parameter input using any of the validation attributes available. The list below specifies the different validation attributes.

  • ValidateNotNull: Specifies that the value cannot be $null.
  • ValidateNotNullOrEmpty: Specifies that the value cannot be $null or empty.
  • ValidateCount: Specifies the minimum and maximum number of values that a parameter can accept.
  • ValidateSet: Specifies a set of valid values.
  • ValidateScript: Specifies a script that is used to validate. The script shall return either - $true or $false.
  • ValidateRange: Specifies a numeric range.
  • ValidatePattern: Specifies a regular expression.
  • ValidateLength: Specifies the minimum and maximum number of characters.

We recommend using validation if you except a specific type of input, such as a range of numbers or specific values.

Adding pipeline functionality

Many of the built-in CmdLets in Windows PowerShell support values from a pipeline. This basically means that you can get a set of objects from a file or Active-Directory or any other source and pipe them to a CmdLet that supports values from pipeline input. You can add this functionality to your functions or scripts using any of the following attributes:

  • ValueFromPipeLine: Specifies that the parameter accepts an entire object as input such as an array or a list of values.
  • ValueFromPipelineByPropertyName: Specifies that the parameter accepts a specific property with the same name as the parameter.

The pipeline functionality also requires that the process block is used.

function Get-TSOperatingSystem { [cmdletbinding(SupportsShouldProcess = $true)] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$ComputerName ) process { if ($psCmdlet.ShouldProcess("Get-TSOperatingSystem", $ComputerName)) { Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName } } }

We recommend adding pipeline support to your functions.

Comment Based Help

Cmdlets in Windows PowerShell include a help-topic that describes how to use the cmdlets in Windows PowerShell which we can retrieve using the Get-Help cmdlet. When writing functions we can add a custom comment-based help topic using a comment block (”<#” and ”#>” ) containing at least one keyword. The valid keywords we can use include: Synopsis, description, parameter (followed by parameter-name), example, inputs, outputs, notes, links, component, role, functionality, example and more. We can retrieve the comment-based help topic for a script using the Get-Help cmdlet. The example below demonstrates how you add a comment-based help topic to a function in Windows PowerShell.

function Get-TSOperatingSystem { <# .SYNOPSIS Get operating system information. .DESCRIPTION Displays operating system information using Win32_OperatingSystem on ne or more computers. .PARAMETER ComputerName Specifies the computers name. .EXAMPLE "SRV01",SRV02" | Get-TSOperatingSystem The example above displays operating system information from SRV01 and SRV02. #> [cmdletbinding(SupportsShouldProcess = $true)] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$ComputerName ) process { if ($psCmdlet.ShouldProcess("Get-TSOperatingSystem", $ComputerName)) { Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName } } }

We recommend:

  • adding comment based help to your functions. This helps the user to understand how a function works and how it is expected to be used.
  • Using PlatyPS to generate help from markdown.

Output from functions

functions in PowerShell should return objects. To return custom objects you can use the PSObject class.

PSObject

The example below demonstrates how to output custom objects from your functions.

function Get-TSOperatingSystem { <# .SYNOPSIS Get operating system information. .DESCRIPTION Displays operating system information using Win32_OperatingSystem on ne or more computers. .PARAMETER ComputerName Specifies the computers name. .EXAMPLE "SRV01",SRV02" | Get-TSOperatingSystem The example above displays operating system information from SRV01 and SRV02. #> [cmdletbinding(SupportsShouldProcess = $true)] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$ComputerName ) process { if ($psCmdlet.ShouldProcess("Get-TSOperatingSystem", $ComputerName)) { $operatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName [hashtable]$properties = @{ ComputerName = $ComputerName OperatingSystem = $operatingSystem.Caption Version = $operatingSystem.Version } New-Object PSObject -Property $properties } } }

We recommed that:

  • functions should be designed to output objects.
  • Do not use Write-Host.
  • Do not output multiple lines of text, use Verbose output instead.

Verbose Output

You can add verbose message output to your functions using either the $psCmdlet automatic variable or the Write-Verbose cmdlet to write data to the verbose stream. This allows you to add verbose information if needed. The example below demonstrates how to add verbose data to a function.

function Get-TSOperatingSystem { <# .SYNOPSIS Get operating system information. .DESCRIPTION Displays operating system information using Win32_OperatingSystem on ne or more computers. .PARAMETER ComputerName Specifies the computers name. .EXAMPLE "SRV01",SRV02" | Get-TSOperatingSystem The example above displays operating system information from SRV01 and SRV02. #> [cmdletbinding(SupportsShouldProcess = $true)] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$ComputerName ) process { if ($psCmdlet.ShouldProcess("Get-TSOperatingSystem", $ComputerName)) { Write-Verbose -Message "Retrieving operating system information from $ComputerName" $operatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName [hashtable]$properties = @{ ComputerName = $ComputerName OperatingSystem = $operatingSystem.Caption Version = $operatingSystem.Version } New-Object PSObject -Property $properties } } }

Handling Errors

Implementing try-catch in your functions allows you to control what happens if something goes wrong. This is useful if a command fails or if a remote computer is offline. Instead of running through the rest of the code you can break your function so that no unnecessary operations are performed. The example below demonstrates how to implement try-catch in a function.

function Get-TSOperatingSystem { <# .SYNOPSIS Get operating system information. .DESCRIPTION Displays operating system information using Win32_OperatingSystem on ne or more computers. .PARAMETER ComputerName Specifies the computers name. .EXAMPLE "SRV01",SRV02" | Get-TSOperatingSystem The example above displays operating system information from SRV01 and SRV02. #> [cmdletbinding(SupportsShouldProcess = $true)] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [string]$ComputerName ) process { if ($psCmdlet.ShouldProcess("Get-TSOperatingSystem", $ComputerName)) { Write-Verbose -Message "Retrieving operating system information from $ComputerName" try { $operatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $ComputerName -ErrorAction Stop } catch { # Add Additional Error Handling here Write-Error $_ break } [hashtable]$properties = @{ ComputerName = $ComputerName OperatingSystem = $operatingSystem.Caption Version = $operatingSystem.Version } New-Object PSObject -Property $properties } } }

We recommend that:

  • Using try-catch with a break execution of a function if something goes wrong, the rest of the code will not be executed
  • Do not use empty catch blocks. If an error occurs the user should be aware of it.
  • When using try-catch with a PowerShell cmdlet, add ErrorAction Stop.
  • Avoid using ErrorAction SilentlyContinue.

Modules

A module in Windows PowerShell is a package containing commands such as cmdlets, providers and functions. Modules are imported automatically whenever you call a command that derives from a module. Commands in a module are easy to find, either by using Get-Module using the ListAvailable switch parameter or by using the integrated command panel in Windows PowerShell ISE. Note that only modules that are placed in a location specified by the PSModulePath are imported automatically. Modules placed in other paths shall be imported using the Import-Module CmdLet.

You can create your own modules by placing any type of Windows PowerShell function or code in a file that ends with .psm1. The nice thing about modules is that it allows you to organize your commands and easily share the module with your colleagues. Note that a modules folder shall have the same name as the module file.

Importing a module from a specific path

PS > Import-Module C:\Temp\TS.System PS > Get-Command -Module TS.System CommandType Name Source ----------- ---- ------ Function Get-TSOperatingSystem TS.System

Importing a module from PSModulePath

PS > Import-Module TS.System PS > Get-Command -Module TS.System CommandType Name Source ----------- ---- ------ Function Get-TSOperatingSystem TS.System

Module Manifest

A module manifest describes a modules content and requirements. You can also add information such as Author, Company, Copyright information and more. It should at least meet the following criteria:

  • Should have a unique guid.
  • Should have a module version.
  • Should have an author.
  • Should have a company name.
  • Should have a copyright.
  • Should have a description.
  • Should have a minimum PowerShell version.
  • Should specify which functions to export.
  • Could specify which CmdLets to export.
  • Should have a complete file list.
  • Should have a help info uri.
  • Could import dependencies (nested modules, assemblies).
PS > New-ModuleManifest -Path "C:\test\TS.System\TS.System.psd1"

The example below demonstrates a module manifest file.

@{ RootModule = 'TS.System.psm1' ModuleVersion = '1.0.0.0' GUID = '27138c41-efce-452c-afdc-97fec2f84102' Author = 'Truesec Technology Organization' CompanyName = 'Truesec' Copyright = '(c) 2026 Truesec. All rights reserved.' Description = 'This module helps users manage windows systems.' PowerShellVersion = '5.0' FunctionsToExport = @( 'Get-TSOperatingSystem' ) ModuleList = @('TS.System.psm1') FileList = @( 'TS.System.psm1', 'TS.System.psd1' ) HelpInfoURI = 'http://www.truesec.com' }

We recommend:

  • Adding manifests to your modules.
  • Using the versioning possibilities in your module manifests.

Appendix

Principles Patterns and Practices

  • Contains no commented out code.
  • Contains no duplicated code (DRY).
  • Contains no dead code.
  • Functions are small.
  • Functions do only one thing and have no side effects.
  • Contains error handling (try/catch) when appropriate.
  • Modules conform to the Single Responsibility Principle. I.e. A Module should be responsible of a single part.
  • The code expresses the intent of the programmer and is easy to understand and follow.
  • Modules and Functions have meaningful names.
  • Function names should say/explain what they do.
  • Should have variable names that explain their purpose.

Rules of simple design

Kent Beck’s four rules of Simple Design

  • Passes the tests
  • Reveals intention
  • No duplication
  • Fewest elements

Runs all tests

  • A system that cannot be verified should never be deployed
  • Testing pushes us towards better designs by:
    • Keeping our functions small
    • Conforming to the Single Responsibility Principle.
    • Reduce coupling
    • Tests gives us confidence to refactor and improve the design even more.

Expresses the intent of the programmer

  • The majority of the cost of a software project is in long-term maintenance.
  • The clearer the author can make the code the less time others will have to spend understanding it.

Contains no duplication

  • Duplication is the primary enemy of a well-designed system

Minimizes the number of Modules and Functions

  • We have this rule just to remind us not to get stuck in pointless dogmatism where we spawn trillions of small functions and modules.
  • We strive at this but although it is important having tests, eliminating duplication and expressing yourself is more important.
Last updated on