DSC Resource Style Guidelines & Best Practices
October 18, 2019 · View on GitHub
In order to provide clean and consistent code, please follow the style guidelines listed below when contributing to any DSC Resource Kit repositories.
It is recommended to also follow the guidance from the best practices section, although this is not required unless the module aims to meet the High Quality Resource Module standards.
Table of Contents
- Style Guidelines
- Markdown Files
- General
- Whitespace
- Indentation
- No Trailing Whitespace After Backticks
- Newline at End of File
- Newline Character Encoding
- No More Than Two Consecutive Newlines
- One Newline Before Braces
- One Newline After Opening Brace
- Two Newlines After Closing Brace
- One Space Between Type and Variable Name
- One Space on Either Side of Operators
- One Space Between Keyword and Parenthesis
- Functions
- Parameters
- Variables
- Best Practices
- General Best Practices
- Calling Functions
- Writing Functions
- Avoid Default Values for Mandatory Parameters
- Avoid Default Values for Switch Parameters
- Include the Force Parameter in Functions with the ShouldContinue Attribute
- Use ShouldProcess if the ShouldProcess Attribute is Defined
- Define the ShouldProcess Attribute if the Function Calls ShouldProcess
- Avoid Redefining Reserved Parameters
- Use the CmdletBinding Attribute on Every Function
- Define the OutputType Attribute for All Functions With Output
- Return Only One Object From Each Function
- DSC Resource Functions
- Return a Hashtable from Get-TargetResource
- Return a Boolean from Test-TargetResource
- Avoid Returning Anything From Set-TargetResource
- Define Get-TargetResource, Set-TargetResource, and Test-TargetResource for Every DSC Resource
- Get-TargetResource should not contain unused non-mandatory parameters
- Any unused parameters that must be included in a function definition should include 'Not used in <function_name>' in the help comment for that parameter in the comment-based help
- Use Identical Parameters for Set-TargetResource and Test-TargetResource
- Use Write-Verbose At Least Once in Get-TargetResource, Set-TargetResource, and Test-TargetResource
- Use *-TargetResource for Exporting DSC Resource Functions
- Manifests
- Localization
- Pester Tests
Style Guidelines
Markdown Files
If a paragraph includes more than one sentence, end each sentence with a newline.
GitHub will still render the sentences as a single paragraph, but the readability
of git diff will be greatly improved.
General
Correct File Encoding
Make sure all files are encoded using UTF-8 (not UTF-8 with BOM), except mof files
which should be encoded using ASCII.
You can use ConvertTo-UTF8 and ConvertTo-ASCII to convert a file to
UTF-8 or ASCII.
Descriptive Names
Use descriptive, clear, and full names for all variables, parameters, and functions. All names must be at least more than 2 characters. No abbreviations should be used.
Bad:
$r = Get-RdsHost
Bad:
$frtytw = 42
Bad:
function Get-Thing
{
...
}
Bad:
function Set-ServerName
{
param
(
$mySTU
)
...
}
Good:
$remoteDesktopSessionHost = Get-RemoteDesktopSessionHost
Good:
$fileCharacterLimit = 42
Good:
function Get-ArchiveFileHandle
{
...
}
Good:
function Set-ServerName
{
param
(
[Parameter()]
$myServerToUse
)
...
}
Correct Parameter Usage in Function and Cmdlet Calls
Use named parameters for function and cmdlet calls rather than positional parameters. Named parameters help other developers who are unfamiliar with your code to better understand it.
When calling a function with many long parameters, use parameter splatting. If splatting is used, then all the parameters should be in the splat. More help on splatting can be found in the article About Splatting.
Make sure hashtable parameters are still properly formatted with multiple lines and the proper indentation.
Bad:
Not using named parameters.
Get-ChildItem C:\Documents *.md
Bad:
The call is very long and will wrap a lot in the review tool when the code is viewed by the reviewer during the review process of the PR.
$mySuperLongHashtableParameter = @{
MySuperLongKey1 = 'MySuperLongValue1'
MySuperLongKey2 = 'MySuperLongValue2'
}
$superLongVariableName = Get-MySuperLongVariablePlease -MySuperLongHashtableParameter $mySuperLongHashtableParameter -MySuperLongStringParameter '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890' -Verbose
Bad:
Hashtable is not following Correct Format for Hashtables or Objects.
$superLongVariableName = Get-MySuperLongVariablePlease -MySuperLongHashtableParameter @{ MySuperLongKey1 = 'MySuperLongValue1'; MySuperLongKey2 = 'MySuperLongValue2' } -MySuperLongStringParameter '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890' -Verbose
Bad:
Hashtable is not following Correct Format for Hashtables or Objects.
$superLongVariableName = Get-MySuperLongVariablePlease -MySuperLongHashtableParameter @{ MySuperLongKey1 = 'MySuperLongValue1'; MySuperLongKey2 = 'MySuperLongValue2' } `
-MySuperLongStringParameter '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890' `
-Verbose
Bad:
Hashtable is not following Correct Format for Hashtables or Objects.
$superLongVariableName = Get-MySuperLongVariablePlease `
-MySuperLongHashtableParameter @{ MySuperLongKey1 = 'MySuperLongValue1'; MySuperLongKey2 = 'MySuperLongValue2' } `
-MySuperLongStringParameter '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890' `
-Verbose
Bad:
Passing parameter (Verbose) outside of the splat.
$getMySuperLongVariablePleaseParameters = @{
MySuperLongHashtableParameter = @{
MySuperLongKey1 = 'MySuperLongValue1'
MySuperLongKey2 = 'MySuperLongValue2'
}
MySuperLongStringParameter = '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'
}
$superLongVariableName = Get-MySuperLongVariablePlease @getMySuperLongVariablePleaseParameters -Verbose
Good:
Get-ChildItem -Path C:\Documents -Filter *.md
Good:
$superLongVariableName = Get-MyVariablePlease -MyStringParameter '123456789012349012345678901234567890' -Verbose
Good:
$superLongVariableName = Get-MyVariablePlease -MyString1 '1234567890' -MyString2 '1234567890' -MyString3 '1234567890' -Verbose
Good:
$mySuperLongHashtableParameter = @{
MySuperLongKey1 = 'MySuperLongValue1'
MySuperLongKey2 = 'MySuperLongValue2'
}
$superLongVariableName = Get-MySuperLongVariablePlease -MySuperLongHashtableParameter $mySuperLongHashtableParameter -Verbose
Good:
Splatting all parameters.
$getMySuperLongVariablePleaseParameters = @{
MySuperLongHashtableParameter = @{
MySuperLongKey1 = 'MySuperLongValue1'
MySuperLongKey2 = 'MySuperLongValue2'
}
MySuperLongStringParameter = '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890'
Verbose = $true
}
$superLongVariableName = Get-MySuperLongVariablePlease @getMySuperLongVariablePleaseParameters
Good:
$superLongVariableName = Get-MySuperLongVariablePlease `
-MySuperLongHashtableParameter @{
MySuperLongKey1 = 'MySuperLongValue1'
MySuperLongKey2 = 'MySuperLongValue2'
} `
-MySuperLongStringParameter '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890' `
-Verbose
Correct Format for Arrays
Arrays should be written in one of the following formats.
If an array is declared on a single line, then there should be a single space between each element in the array. If arrays written on a single line tend to be long, please consider using one of the alternative ways of writing the array.
Bad:
Array elements are not format consistently.
$array = @( 'one', `
'two', `
'three'
)
Bad:
There are no single space beetween the elements in the array.
$array = @('one','two','three')
Bad:
There are multiple array elements on the same row.
$array = @(
'one', 'two', `
'my long string example', `
'three', 'four'
)
Bad:
Hashtable is not following Correct Format for Hashtables or Objects.
$array = @(
'one',
@{MyKey = 'MyValue'},
'three'
)
Bad:
Hashtables are not following Correct Format for Hashtables or Objects.
$myArray = @(
@{Key1 = Value1;Key2 = Value2},
@{Key1 = Value1;Key2 = Value2}
)
Good:
$array = @('one', 'two', 'three')
Good:
$array = @(
'one',
'two',
'three'
)
Good:
$array = @(
'one'
'two'
'three'
)
Good:
$hashtable = @{
Key = "Value"
}
$array = @( 'one', 'two', 'three', $hashtable )
Good:
$hashtable = @{
Key = "Value"
}
$array = @(
'one',
'two',
'three',
$hashtable
)
Good:
$myArray = @(
@{
Key1 = Value1
Key2 = Value2
},
@{
Key1 = Value1
Key2 = Value2
}
)
Correct Format for Hashtables or Objects
Hashtables and Objects should be written in the following format. Each property should be on its own line indented once.
Bad:
$hashtable = @{
}
Bad:
$hashtable = @{ }
Bad:
$hashtable = @{Key1 = 'Value1';Key2 = 2;Key3 = '3'}
Bad:
$hashtable = @{ Key1 = 'Value1'
Key2 = 2
Key3 = '3' }
Good:
$hashtable = @{}
Good:
$hashtable = @{
Key1 = 'Value1'
Key2 = 2
Key3 = '3'
}
Good:
$hashtable = @{
Key1 = 'Value1'
Key2 = 2
Key3 = @{
Key3Key1 = 'ExampleText'
Key3Key2 = 42
}
}
Correct use of single- and double quotes
Single quotes should always be used to delimit string literals wherever possible. Double quoted string literals may only be used when it contains ($) expressions that need to be evaluated.
Bad:
$string = "String that do not evaluate variable"
Bad:
$string = "String that evaluate variable {0}" -f $SomeObject.SomeProperty
Good:
$string = 'String that do not evaluate variable'
Good:
$string = 'String that evaluate variable {0}' -f $SomeObject.SomeProperty
Good:
$string = "String that evaluate variable $($SomeObject.SomeProperty)"
Good:
$string = 'String that evaluate variable ''{0}''' -f $SomeObject.SomeProperty
Good:
$string = "String that evaluate variable '{0}'" -f $SomeObject.SomeProperty
Correct Format for Comments
There should not be any commented-out code in checked-in files. The first letter of the comment should be capitalized.
Single line comments should be on their own line and start with a single pound-sign followed by a single space. The comment should be indented the same amount as the following line of code.
Comments that are more than one line should use the <# #> format rather
than the single pound-sign. The opening and closing brackets should be on their
own lines. The comment inside the brackets should be indented once more than the
brackets. The brackets should be indented the same amount as the following line
of code.
Formatting help-comments for functions has a few more specific rules that can be found here.
Bad:
function Get-MyVariable
{#this is a bad comment
[CmdletBinding()]
param ()
#this is a bad comment
foreach ($example in $examples)
{
Write-Verbose -Message $example #this is a bad comment
}
}
Bad:
function Get-MyVariable
{
[CmdletBinding()]
param ()
# this is a bad comment
# On multiple lines
foreach ($example in $examples)
{
# No commented-out code!
# Write-Verbose -Message $example
}
}
Good:
function Get-MyVariable
{
# This is a good comment
[CmdletBinding()]
param ()
# This is a good comment
foreach ($example in $examples)
{
# This is a good comment
Write-Verbose -Message $example
}
}
Good:
function Get-MyVariable
{
[CmdletBinding()]
param ()
<#
This is a good comment
on multiple lines
#>
foreach ($example in $examples)
{
Write-Verbose -Message $example
}
}
Correct Format for Keywords
PowerShell reserved Keywords should be in all lower case and should be immediately followed by a space if there is non-whitespace characters following (for example, an open brace).
Some reserved Keywords may also be followed by an open curly brace, for
example the catch keyword. These keywords that are followed by a
curly brace should also follow the One Newline Before Braces
guideline.
The following is the current list of PowerShell reserved keywords in PowerShell 5.1:
begin, break, catch, class, continue, data, define do, dynamicparam, else,
elseif, end, enum, exit, filter, finally, for, foreach, from, function
hidden, if, in, inlinescript, param, process, return, static, switch,
throw, trap, try, until, using, var, while
This list may change in newer versions of PowerShell.
The latest list of PowerShell reserved keywords can also be found on this page.
Bad:
# Missing space after keyword and before open bracket
foreach($item in $list)
Bad:
# Capital letters in keyword
BEGIN
Bad:
# Violates 'One Newline Before Braces' guideline
begin {
# Do some work
}
Bad:
# Capital letters in 'in' and 'foreach' keyword
ForEach ($item In $list)
Good:
foreach ($item in $list)
Good:
begin
{
# Do some work
}
Whitespace
Indentation
For all indentation, use 4 spaces instead of tabs. There should be no tab characters in the file unless they are in a here-string.
No Trailing Whitespace After Backticks
Backticks should always be directly followed by a newline
Newline at End of File
All files must end with a newline, see StackOverflow.
Newline Character Encoding
Save newlines using CR+LF instead of CR. For interoperability reasons, we recommend that you follow these instructions when installing Git on Windows so that newlines saved to GitHub are simply CRs.
No More Than Two Consecutive Newlines
Code should not contain more than two consecutive newlines unless they are contained in a here-string.
Bad:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
return $MyValue
}
Bad:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
return $MyValue
}
function Write-Log
{
Write-Verbose -Message 'Logging...'
}
Good:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
return $MyValue
}
Good:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
return $MyValue
}
function Write-Log
{
Write-Verbose -Message 'Logging...'
}
One Newline Before Braces
Each curly brace should be preceded by a newline unless assigning to a variable.
Bad:
if ($booleanValue) {
Write-Verbose -Message "Boolean is $booleanValue"
}
Good:
if ($booleanValue)
{
Write-Verbose -Message "Boolean is $booleanValue"
}
When assigning to a variable, opening curly braces should be on the same line as the assignment operator.
Bad:
$scriptBlockVariable =
{
Write-Verbose -Message 'Executing script block'
}
Bad:
$hashtableVariable =
@{
Key1 = 'Value1'
Key2 = 'Value2'
}
Good:
$scriptBlockVariable = {
Write-Verbose -Message 'Executing script block'
}
Good:
$hashtableVariable = @{
Key1 = 'Value1'
Key2 = 'Value2'
}
One Newline After Opening Brace
Each opening curly brace should be followed by only one newline.
Bad:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
return $MyValue
}
Bad:
function Get-MyValue
{ Write-Verbose -Message 'Getting MyValue'
return $MyValue
}
Good:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
return $MyValue
}
Two Newlines After Closing Brace
Each closing curly brace ending a function, conditional block, loop, etc. should be followed by exactly two newlines unless it is directly followed by another closing brace. If the closing brace is followed by another closing brace or continues a conditional or switch block, there should be only one newline after the closing brace.
Bad:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
return $MyValue
} Get-MyValue
Bad:
function Get-MyValue
{ Write-Verbose -Message 'Getting MyValue'
if ($myBoolean)
{
return $MyValue
}
else
{
return 0
}
}
Get-MyValue
Good:
function Get-MyValue
{
Write-Verbose -Message 'Getting MyValue'
if ($myBoolean)
{
return $MyValue
}
else
{
return 0
}
}
Get-MyValue
One Space Between Type and Variable Name
If you must declare a variable type, type declarations should be separated from the variable name by a single space.
Bad:
function Get-TargetResource
{
[CmdletBinding()]
param ()
[Int]$number = 2
}
Good:
function Get-TargetResource
{
[CmdletBinding()]
param ()
[Int] $number = 2
}
One Space on Either Side of Operators
There should be one blank space on either side of all operators.
Bad:
function Get-TargetResource
{
[CmdletBinding()]
param ()
$number=2+4-5*9/6
}
Bad:
function Get-TargetResource
{
[CmdletBinding()]
param ()
if ('example'-eq'example'-or'magic')
{
Write-Verbose -Message 'Example found.'
}
}
Good:
function Get-TargetResource
{
[CmdletBinding()]
param ()
$number = 2 + 4 - 5 * 9 / 6
}
Good:
function Get-TargetResource
{
[CmdletBinding()]
param ()
if ('example' -eq 'example' -or 'magic')
{
Write-Verbose -Message 'Example found.'
}
}
One Space Between Keyword and Parenthesis
If a keyword is followed by a parenthesis, there should be single space between the keyword and the parenthesis.
Bad:
function Get-TargetResource
{
[CmdletBinding()]
param ()
if('example' -eq 'example' -or 'magic')
{
Write-Verbose -Message 'Example found.'
}
foreach($example in $examples)
{
Write-Verbose -Message $example
}
}
Good:
function Get-TargetResource
{
[CmdletBinding()]
param ()
if ('example' -eq 'example' -or 'magic')
{
Write-Verbose -Message 'Example found.'
}
foreach ($example in $examples)
{
Write-Verbose -Message $example
}
}
Functions
Function Names Use Pascal Case
Function names must use PascalCase. This means that each concatenated word is capitalized.
Bad:
function get-targetresource
{
# ...
}
Good:
function Get-TargetResource
{
# ...
}
Function Names Use Verb-Noun Format
All function names must follow the standard PowerShell Verb-Noun format.
Bad:
function TargetResourceGetter
{
# ...
}
Good:
function Get-TargetResource
{
# ...
}
Function Names Use Approved Verbs
All function names must use approved verbs.
Bad:
function Normalize-String
{
# ...
}
Good:
function ConvertTo-NormalizedString
{
# ...
}
Functions Have Comment-Based Help
All functions should have comment-based help with the correct syntax directly above the function. Comment-help should include at least the SYNOPSIS section and a PARAMETER section for each parameter.
Bad:
# Creates an event
function New-Event
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter()]
[ValidateSet('operational', 'debug', 'analytic')]
[String]
$Channel = 'operational'
)
# Implementation...
}
Good:
<#
.SYNOPSIS
Creates an event
.PARAMETER Message
Message to write
.PARAMETER Channel
Channel where message should be stored
.EXAMPLE
New-Event -Message 'Attempting to connect to server' -Channel 'debug'
#>
function New-Event
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter()]
[ValidateSet('operational', 'debug', 'analytic')]
[String]
$Channel = 'operational'
)
# Implementation
}
Parameter Block at Top of Function
There must be a parameter block declared for every function. The parameter block must be at the top of the function and not declared next to the function name. Functions with no parameters should still display an empty parameter block.
Bad:
function Write-Text([Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][String]$Text)
{
Write-Verbose -Message $Text
}
Bad:
function Write-Nothing
{
Write-Verbose -Message 'Nothing'
}
Good:
function Write-Text
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Text
)
Write-Verbose -Message $Text
}
Good:
function Write-Nothing
{
param ()
Write-Verbose -Message 'Nothing'
}
Parameters
Correct Format for Parameter Block
- An empty parameter block should be displayed on its own line like this:
param (). - A non-empty parameter block should have the opening and closing parentheses on their own line.
- All text inside the parameter block should be indented once.
- Every parameter should include the
[Parameter()]attribute, regardless of whether the attribute requires decoration or not. - A parameter that is mandatory should contain this decoration:
[Parameter(Mandatory = $true)]. - A parameter that is not mandatory should not contain a
Mandatorydecoration in the[Parameter()].
Bad:
function Write-Nothing
{
param
(
)
Write-Verbose -Message 'Nothing'
}
Bad:
function Write-Text
{
param([Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String] $Text )
Write-Verbose -Message $Text
}
Bad:
function Write-Text
{
param
(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[String]
$Text
[Parameter(Mandatory = $false)]
[ValidateNotNullOrEmpty()]
[String]
$PrefixText
[Boolean]
$AsWarning = $false
)
if ($AsWarning)
{
Write-Warning -Message "$PrefixText - $Text"
}
else
{
Write-Verbose -Message "$PrefixText - $Text"
}
}
Good:
function Write-Nothing
{
param ()
Write-Verbose -Message 'Nothing'
}
Good:
function Write-Text
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Text
)
Write-Verbose -Message $Text
}
Good:
function Write-Text
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Text
[Parameter()]
[ValidateNotNullOrEmpty()]
[String]
$PrefixText
[Parameter()]
[Boolean]
$AsWarning = $false
)
if ($AsWarning)
{
Write-Warning -Message "$PrefixText - $Text"
}
else
{
Write-Verbose -Message "$PrefixText - $Text"
}
}
Parameter Names Use Pascal Case
All parameters must use PascalCase. This means that each concatenated word is capitalized.
Bad:
function Get-TargetResource
{
[CmdletBinding()]
param
(
$SOURCEPATH
)
}
Bad:
function Get-TargetResource
{
[CmdletBinding()]
param
(
$sourcepath
)
}
Good:
function Get-TargetResource
{
[CmdletBinding()]
param
(
[Parameter()]
$SourcePath
)
}
Parameters Separated by One Line
Parameters must be separated by a single, blank line.
Bad:
function New-Event
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[ValidateSet('operational', 'debug', 'analytic')]
[String]
$Channel = 'operational'
)
}
Good:
function New-Event
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter()]
[ValidateSet('operational', 'debug', 'analytic')]
[String]
$Channel = 'operational'
)
}
Parameter Type on Line Above
The parameter type must be on its own line above the parameter name. If an attribute needs to follow the type, it should also have its own line between the parameter type and the parameter name.
Bad:
function Get-TargetResource
{
[CmdletBinding()]
param
(
[String] $SourcePath = 'c:\'
)
}
Good:
function Get-TargetResource
{
[CmdletBinding()]
param
(
[Parameter()]
[String]
$SourcePath = 'c:\'
)
}
Good:
function Get-TargetResource
{
[CmdletBinding()]
param
(
[Parameter()]
[PSCredential]
[Credential()]
$MyCredential
)
}
Good:
function New-Event
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter()]
[ValidateSet('operational', 'debug', 'analytic')]
[String]
$Channel = 'operational'
)
}
Parameter Attributes on Separate Lines
Parameter attributes should each have their own line. All attributes should go above the parameter type, except those that must be between the type and the name.
Bad:
function New-Event
{
param
(
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][String]
$Message,
[ValidateSet('operational', 'debug', 'analytic')][String]
$Channel = 'operational'
)
}
Good:
function New-Event
{
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Message,
[Parameter()]
[ValidateSet('operational', 'debug', 'analytic')]
[String]
$Channel = 'operational'
)
}
Variables
Variable Names Use Camel Case
Variable names should use camelCase.
Bad:
function Write-Log
{
$VerboseMessage = 'New log message'
Write-Verbose $VerboseMessage
}
Bad:
function Write-Log
{
$verbosemessage = 'New log message'
Write-Verbose $verbosemessage
}
Good:
function Write-Log
{
$verboseMessage = 'New log message'
Write-Verbose $verboseMessage
}
Script, Environment and Global Variable Names Include Scope
Script, environment, and global variables must always include their scope in the variable name unless the 'using' scope is needed. The script and global scope specifications should be all in lowercase. Script and global variable names following the scope should use camelCase.
Bad:
$fileCount = 0
$GLOBAL:MYRESOURCENAME = 'MyResource'
function New-File
{
$fileCount++
Write-Verbose -Message "Adding file to $MYRESOURCENAME to $ENV:COMPUTERNAME."
}
Good:
$script:fileCount = 0
$global:myResourceName = 'MyResource'
function New-File
{
$script:fileCount++
Write-Verbose -Message "Adding file to $global:myResourceName to $env:computerName."
}
Best Practices
Although adoping the best practices is optional, doing so will help improve the quality of the DSC resource module.
Note: Modules that aim to meet the High Quality Resource Module standards must also implement the best practices whereever possible.
General Best Practices
Avoid Using Hardcoded Computer Name
Using hardcoded computer names exposes sensitive information on your machine. Use a parameter or environment variable instead if a computer name is necessary. This comes from this PS Script Analyzer rule.
Bad:
Invoke-Command -Port 0 -ComputerName 'hardcodedName'
Good:
Invoke-Command -Port 0 -ComputerName $env:computerName
Avoid Empty Catch Blocks
Empty catch blocks are not necessary. Most errors should be thrown or at least acted upon in some way. If you really don't want an error to be thrown or logged at all, use the ErrorAction parameter with the SilentlyContinue value instead.
Bad:
try
{
Get-Command -Name Invoke-NotACommand
}
catch {}
Good:
Get-Command -Name Invoke-NotACommand -ErrorAction SilentlyContinue
Ensure Null is on Left Side of Comparisons
When comparing a value to $null, $null should be on the left side of
the comparison.
This is due to an issue in PowerShell.
If $null is on the right side of the comparison and the value you are comparing
it against happens to be a collection, PowerShell will return true if the collection
contains $null rather than if the entire collection actually is $null.
Even if you are sure your variable will never be a collection, for consistency,
please ensure that $null is on the left side of all comparisons.
Bad:
if ($myArray -eq $null)
{
Remove-AllItems
}
Good:
if ($null -eq $myArray)
{
Remove-AllItems
}
Avoid Global Variables
Avoid using global variables whenever possible. These variables can be edited by any other script that ran before your script or is running at the same time as your script. Use them only with extreme caution, and try to use parameters or script/local variables instead.
This rule has a few exceptions:
- The use of
$global:DSCMachineStatusis still recommended to restart a machine from a DSC resource.
Bad:
$global:configurationName = 'MyConfigurationName'
...
Set-MyConfiguration -ConfigurationName $global:configurationName
Good:
$script:configurationName = 'MyConfigurationName'
...
Set-MyConfiguration -ConfigurationName $script:configurationName
Use Declared Local and Script Variables More Than Once
Don't declare a local or script variable if you're not going to use it. This creates excess code that isn't needed
Use PSCredential for All Credentials
PSCredentials are more secure than using plaintext username and passwords.
Bad:
function Get-Settings
{
param
(
[String]
$Username
[String]
$Password
)
...
}
Good:
function Get-Settings
{
param
(
[PSCredential]
[Credential()]
$UserCredential
)
}
Use Variables Rather Than Extensive Piping
This is a script not a console. Code should be easy to follow. There should be no more than 1 pipe in a line. This rule is specific to the DSC Resource Kit - other PowerShell best practices may say differently, but this is our preferred format for readability.
Bad:
Get-Objects | Where-Object { $_.Propery -ieq 'Valid' } | Set-ObjectValue `
-Value 'Invalid' | Foreach-Object { Write-Output $_ }
Good:
$validPropertyObjects = Get-Objects | Where-Object { $_.Property -ieq 'Valid' }
foreach ($validPropertyObject in $validPropertyObjects)
{
$propertySetResult = Set-ObjectValue $validPropertyObject -Value 'Invalid'
Write-Output $propertySetResult
}
Avoid Unnecessary Type Declarations
If it is clear what type a variable is then it is not necessary to explicitly declare its type. Extra type declarations can clutter the code.
Bad:
[String] $myString = 'My String'
Bad:
[System.Boolean] $myBoolean = $true
Good:
$myString = 'My String'
Good:
$myBoolean = $true
Calling Functions
Avoid Cmdlet Aliases
When calling a function use the full command not an alias.
You can get the full command an alias represents by calling Get-Alias.
Bad:
ls -File $root -Recurse | ? { @('.gitignore', '.mof') -contains $_.Extension }
Good:
Get-ChildItem -File $root -Recurse | Where-Object -FilterScript {
@('.gitignore', '.mof') -contains $_.Extension
}
Avoid Invoke-Expression
Invoke-Expression is vulnerable to string injection attacks. It should not be used in any DSC resources.
Bad:
Invoke-Expression -Command "Test-$DSCResourceName"
Good:
& "Test-$DSCResourceName"
Use the Force Parameter with Calls to ShouldContinue
Bad:
Good:
Avoid the WMI Cmdlets
The WMI cmdlets can all be replaced by CIM cmdlets. Use the CIM cmdlets instead because they align with industry standards.
Bad:
Get-WMIInstance -ClassName Win32_Process
Good:
Get-CIMInstance -ClassName Win32_Process
Avoid Write-Host
Write-Host is harmful. Use alternatives such as Write-Verbose, Write-Output, Write-Debug, etc.
Bad:
Write-Host 'Setting the variable to a value.'
Good:
Write-Verbose -Message 'Setting the variable to a value.'
Avoid ConvertTo-SecureString with AsPlainText
SecureStrings should be encrypted. When using ConvertTo-SecureString with the
AsPlainText parameter specified the SecureString text is not encrypted and thus
not secure. This is allowed in tests/examples when needed, but never in the actual
resources.
Bad:
ConvertTo-SecureString -String 'mySecret' -AsPlainText -Force
Assign Function Results to Variables Rather Than Extensive Inline Calls
Bad:
Set-Background -Color (Get-Color -Name ((Get-Settings -User (Get-CurrentUser)).ColorName))
Good:
$currentUser = Get-CurrentUser
$userSettings = Get-Settings -User $currentUser
$backgroundColor = Get-Color -Name $userSettings.ColorName
Set-Background -Color $backgroundColor
Writing Functions
Avoid Default Values for Mandatory Parameters
Default values for mandatory parameters will always be overwritten, thus they are never used and can cause confusion.
Bad:
function Get-Something
{
param
(
[Parameter(Mandatory = $true)]
[String]
$Name = 'My Name'
)
...
}
Good:
function Get-Something
{
param
(
[Parameter(Mandatory = $true)]
[String]
$Name
)
...
}
Avoid Default Values for Switch Parameters
Switch parameters have 2 values - there or not there. The default value is automatically true - don't - refactor your code instead.
Bad:
function Get-Something
{
param
(
[Switch]
$MySwitch = $true
)
...
}
Good:
function Get-Something
{
param
(
[Switch]
$MySwitch
)
...
}
Include the Force Parameter in Functions with the ShouldContinue Attribute
Bad:
Good:
Use ShouldProcess if the ShouldProcess Attribute is Defined
Bad:
Good:
Define the ShouldProcess Attribute if the Function Calls ShouldProcess
Bad:
Good:
Avoid Redefining Reserved Parameters
Reserved Parameters such as Verbose, Debug, etc. are already added to the function at runtime so don't redefine them. Add the CmdletBinding attribute to include the reserved parameters in your function.
Use the CmdletBinding Attribute on Every Function
The CmdletBinding attribute adds the reserved parameters to your function which is always preferable.
Bad:
function Get-Property
{
param
(
...
)
...
}
Good:
function Get-Property
{
[CmdletBinding()]
param
(
...
)
...
}
Define the OutputType Attribute for All Functions With Output
The OutputType attribute should be declared if the function has output so that the correct error messages get displayed if the function ever produces an incorrect output type.
Bad:
function Get-MyBoolean
{
[OutputType([Boolean])]
param ()
...
return $myBoolean
}
Good:
function Get-MyBoolean
{
[CmdletBinding()]
[OutputType([Boolean])]
param ()
...
return $myBoolean
}
Return Only One Object From Each Function
Bad:
Good:
DSC Resource Functions
Return a Hashtable from Get-TargetResource
Bad:
Good:
Return a Boolean from Test-TargetResource
Bad:
Good:
Avoid Returning Anything From Set-TargetResource
Bad:
Good:
Define Get-TargetResource, Set-TargetResource, and Test-TargetResource for Every DSC Resource
Bad:
Good:
Get-TargetResource should not contain unused non-mandatory parameters
The inclusion of a non-mandatory parameter that is never used could signal that
there is a design flaw in the implementation of the Get-TargetResource function.
The non-mandatory parameters that are used to call Get-TargetResource should help
to retrieve the actual values of the properties for the resource.
For example, if there is a parameter Ensure that is non-mandatory, that parameter
describes the state the resource should have, but it might not be used to retrieve
the actual values.
Another example would be if a parameter FilePathName is set to be non-mandatory,
but FilePathName is actually a property that Get-TargetResource should return
the actual value of.
In that case it does not make sense to assign a value to FilePathName when
calling Get-TargetResource because that value will never be used.
Bad:
<#
.SYNOPSIS
Returns the current state of the feature.
.PARAMETER Name
The feature for which to return the state for.
.PARAMETER ServerName
The server name on which the feature is installed.
.PARAMETER Ensure
The desired state of the feature.
#>
function Get-TargetResource
{
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Name,
[Parameter(Mandatory = $true)]
[System.String]
$ServerName,
[Parameter()]
[System.String]
$Ensure
)
Write-Verbose -Message ('{0} for {1}' -f $Name, $ServerName)
if( $Name )
{
$feature = 'Enabled'
}
return @{
Name = $Name
Servername = $ServerName
Feeature = $feature
}
}
Good:
<#
.SYNOPSIS
Returns the current state of the feature.
.PARAMETER Name
The feature for which to return the state for.
.PARAMETER ServerName
The server name on which the feature is installed.
#>
function Get-TargetResource
{
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Name,
[Parameter(Mandatory = $true)]
[System.String]
$ServerName
)
Write-Verbose -Message ('{0} for {1}' -f $Name, $ServerName)
if( $Name )
{
$feature = 'Yes'
}
return @{
Name = $Name
Servername = $ServerName
Feeature = $feature
}
}
Any unused parameters that must be included in a function definition should include 'Not used in <function_name>' in the help comment for that parameter in the comment-based help
The inclusion of a mandatory parameter in the 'Get-TargetResource' function that is never used could signal that there is a design flaw in the implementation of the function. The mandatory parameters that are used to call 'Get-TargetResource' should help to retrieve the actual values of the properties for the resource. For example, if there is a parameter 'Ensure' that is mandatory, that parameter will not be used to retrieve the actual values. Another example would be if a parameter 'FilePathName' is set to be mandatory, but 'FilePathName' is actually a property that 'Get-TargetResource' should return the actual value of. In that case it does not make sense to assign a value to 'FilePathName' when calling 'Get-TargetResource' because that value will never be used.
The inclusion of a mandatory or a non-mandatory parameter in the Test-TargetResource function that is not used is more common since it is required that both the 'Set-TargetResource' and the 'Test-TargetResource' have the same parameters. Thus, there will be times when not all of the parameters in the 'Test-TargetResource' function will be used in the function.
If there is a need design-wise to include a mandatory parameter that will not be used, then the comment-based help for that parameter should contain the description 'Not used in <function_name>'.
Bad:
<#
.SYNOPSIS
Returns the current state of the feature.
.PARAMETER Name
The feature for which to return the state for.
.PARAMETER ServerName
The server name on which the feature is installed.
.PARAMETER Ensure
The desired state of the feature.
#>
function Get-TargetResource
{
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Name,
[Parameter(Mandatory = $true)]
[System.String]
$ServerName,
[Parameter(Mandatory = $true)]
[System.String]
$Ensure
)
Write-Verbose -Message ('{0} for {1}' -f $Name, $ServerName)
if( $Name )
{
$feature = 'Yes'
}
return @{
Name = $Name
Servername = $ServerName
Feeature = $feature
}
}
Good:
<#
.SYNOPSIS
Returns the current state of the feature.
.PARAMETER Name
The feature for which to return the state for.
.PARAMETER ServerName
The server name on which the feature is installed.
.PARAMETER Ensure
The desired state of the feature.
Not used in Get-TargetResource
#>
function Get-TargetResource
{
param
(
[Parameter(Mandatory = $true)]
[System.String]
$Name,
[Parameter(Mandatory = $true)]
[System.String]
$ServerName,
[Parameter(Mandatory = $true)]
[System.String]
$Ensure
)
Write-Verbose -Message ('{0} for {1}' -f $Name, $ServerName)
if( $Name )
{
$feature = 'Yes'
}
return @{
Name = $Name
Servername = $ServerName
Feeature = $feature
}
}
Use Identical Parameters for Set-TargetResource and Test-TargetResource
Bad:
Good:
Use Write-Verbose At Least Once in Get-TargetResource, Set-TargetResource, and Test-TargetResource
Bad:
Good:
Use *-TargetResource for Exporting DSC Resource Functions
Bad:
Good:
Manifests
Avoid Using Deprecated Manifest Fields
Bad:
Good:
Ensure Manifest Contains Correct Fields
Bad:
Good:
Do not use NestedModules to export shared commands
Since we don't use the RootModule key in the module manifest, we should not
use the NestedModules key to add modules that export commands that are shared
between resource modules.
Normally, a single list in the RootModule key, can restrict what is
exported using the cmdlet Export-ModuleMember. Since we don't use the RootModule
key we can't restrict what is exported, so every nested module will export all the
commands (or all the commands restricted by Export-ModuleMember in that
individual nested module). If two resource modules were to use the NestedModules
method, it would result in one of them being unable to install since they have
conflicting exported commands.
Localization
In each resource folder there should be at least one localization folder for
english language 'en-US'. Add other localization folders as appropriate, the
correct folder name can be found by running Get-UICulture on the node that
has a UI culture installed that the strings are being built for.
There is also the list of
Available Language Packs for Windows.
In each localization folder there should be a PowerShell data (.psd1) file named 'MSFT_<ResourceName>.strings.psd1' (e.g. 'MSFT_Folder.strings.psd1'). Each localized string file should contain the following with the correct localization key and accompanying localization string value (the example uses the friendly resource name of 'Folder').
# Localized resources for Folder
ConvertFrom-StringData @'
CreateFolder = Creating folder at path '{0}'. (F0001)
RetrievingFolderInformation = Retrieving folder information from path '{0}'. (F0002)
ProblemAccessFolder = Could not access the requested path '{0}'. (F0003)
FailedToReadProperties = Could not read property '{0}' of path '{1}'. (F0004)
'@
When using the previous example, the folder structure would look like the following:
DSCResources\MSFT_Folder\en-US\MSFT_Folder.strings.psd1
DSCResources\MSFT_Folder\es-ES\MSFT_Folder.strings.psd1
To use the localization strings in a resource, then localization strings are
imported at the top of each resource PowerShell module script file (.psm1). The
localized strings should be imported using Get-LocalizedData.
Localization string ID suffix
To easier debug localized verbose logs, and to find the correct localized string key in the code, it is recommended to use a hard coded ID on each localized key's string value. That ID must be the same across the localized language files, and it is not recommended to reuse ID's once they become obsolete (because of for example code change).
Using this method we could also test that each localized string is represented in each language specific localization file for that resource.
Format: (ID:yyyyZZZZ)
- 'yyyy' - The resource name prefix. It is composed of every first letter of every word in the resource friendly name.
- 'ZZZZ' - The suffix is a serial number starting from '0001'.
Example of prefixes:
| Module | Resource | ID prefix (yyyy) | First string ID suffix |
|---|---|---|---|
| PSDscResources | GroupResource | GR | (GR0001) |
| PSDscResources | WindowsOptionalFeature | WOF | (WOF0001) |
| SqlServerDsc | SqlSetup | SS | (SS0001) |
| SqlServerDsc | SqlAGDatabase | SAGD | (SAGD0001) |
| NetworkingDsc | DnsClientGlobalSetting | DCGS | (DCGS0001) |
| NetworkingDsc | Firewall | F | (F0001) |
| CertificateDsc | PfxImport | PI | (PI0001) |
| CertificateDsc | WaitForCertificateServices | WFCS | (WFCS0001) |
Example of usage:
See example of localized strings under Localization.
Using localization string in code
This is an example of how to write localized verbose messages.
Write-Verbose -Message `
($script:localizedData.RetrievingFolderInformation -f $path)
This is an example of how to write localized warning messages.
Write-Warning -Message `
($script:localizedData.ProblemAccessFolder -f $path)
This is an example of how to throw localized error messages, but the
helper functions
New-InvalidArgumentException,
New-InvalidOperationException,
New-ObjectNotFoundException and
New-InvalidResultException
should preferably be used whenever possible.
throw ($script:localizedData.FailedToReadProperties -f $property, $path)
Helper functions for localization
There are also five helper functions to simplify localization. These can be be found in the repository DscResource.Template in the module script file DscResource.LocalizationHelper
To use it, copy the module folder DscResource.LocalizationHelper and the unit tests DscResource.LocalizationHelper.Tests.ps1 to the new resource module.
Get-LocalizedData
This should be used at the top of each resource PowerShell module script file (.psm1). Refer to the comment-based help for more information about this helper function.
$script:resourceModulePath = Split-Path `
-Path (Split-Path -Path $PSScriptRoot -Parent) `
-Parent
$script:localizationModulePath = Join-Path `
-Path $script:resourceModulePath `
-ChildPath 'Modules\DscResource.LocalizationHelper'
Import-Module -Name (
Join-Path `
-Path $script:localizationModulePath `
-ChildPath 'DscResource.LocalizationHelper.psm1'
)
$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_SqlSetup'
New-InvalidArgumentException
Refer to the comment-based help for more information about this helper function.
if ( -not $resultOfEvaluation )
{
$errorMessage = `
$script:localizedData.ActionCannotBeUsedInThisContextMessage `
-f $Action, $Parameter
New-InvalidArgumentException -ArgumentName 'Action' -Message $errorMessage
}
New-InvalidOperationException
Refer to the comment-based help for more information about this helper function.
try
{
Start-Process @startProcessArguments
}
catch
{
$errorMessage = $script:localizedData.InstallationFailedMessage -f $Path, $processId
New-InvalidOperationException -Message $errorMessage -ErrorRecord $_
}
New-ObjectNotFoundException
Refer to the comment-based help for more information about this helper function.
try
{
Get-ChildItem -Path $path
}
catch
{
$errorMessage = $script:localizedData.PathNotFoundMessage -f $path
New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_
}
New-InvalidResultException
Refer to the comment-based help for more information about this helper function.
try
{
$numberOfObjects = Get-ChildItem -Path $path
if ($numberOfObjects -eq 0)
{
throw 'To few files.'
}
}
catch
{
$errorMessage = $script:localizedData.TooFewFilesMessage -f $path
New-InvalidResultException -Message $errorMessage -ErrorRecord $_
}
Pester Tests
Capitalized Pester Assertions
Pester assertions should all start with capital letters. This makes code easier to read.
Bad:
it 'Should return something' {
get-targetresource @testParameters | should -be 'something'
}
Good:
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
Assertion Messages Start with Should
Pester assertions should always start with the word 'Should'. This is to ensure the test results read more naturally as well as helping to indentify assertion messages that aren't making assertions.
Bad:
# This is not an assertive message
It 'When calling Get-TargetResource' {
Get-TargetResource @testParameters | Should -Be 'something'
}
Bad:
# This does not start with 'Should'
It 'Something is returned' {
Get-TargetResource @testParameters | Should -Be 'something'
}
Good:
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
Context Block Messages Start with When
Pester test outer Context block messages should always start with the word
'When'. This is to ensure the test results read more naturally as well as helping
to indentify context messages that aren't defining context.
Bad:
# Context block not using 'When'
Context 'Calling Get-TargetResource with default parameters'
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
}
Bad:
Context 'When calling Get-TargetResource'
# Inner context block not using 'When'
Context 'With default parameters'
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
}
}
Good:
Context 'When Get-TargetResource is called with default parameters'
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
}
Good:
Context 'When Get-TargetResource is called'
Context 'When passing default parameters'
It 'Should return something' {
Get-TargetResource @testParameters | Should -Be 'something'
}
}
}