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.
- 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
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.
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.
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
)
...
}
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
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
}
)
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
}
}
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
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
}
}
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
}
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.
Backticks should always be directly followed by a newline
All files must end with a newline, see StackOverflow.
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.
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...'
}
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'
}
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
}
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
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
}
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.'
}
}
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
}
}
Function names must use PascalCase. This means that each concatenated word is capitalized.
Bad:
function get-targetresource
{
# ...
}
Good:
function Get-TargetResource
{
# ...
}
All function names must follow the standard PowerShell Verb-Noun format.
Bad:
function TargetResourceGetter
{
# ...
}
Good:
function Get-TargetResource
{
# ...
}
All function names must use approved verbs.
Bad:
function Normalize-String
{
# ...
}
Good:
function ConvertTo-NormalizedString
{
# ...
}
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
}
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'
}
- 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
Mandatory
decoration 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"
}
}
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 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'
)
}
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 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'
)
}
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 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."
}
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.
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
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
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 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:DSCMachineStatus
is 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
Don't declare a local or script variable if you're not going to use it. This creates excess code that isn't needed
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
)
}
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
}
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
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
}
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"
Bad:
Good:
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
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.'
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
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
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
)
...
}
Switch parameters have 2 values - there or not there. The default value is automatically $false so it doesn't need to be declared. If you are tempted to set the default value to $true - don't - refactor your code instead.
Bad:
function Get-Something
{
param
(
[Switch]
$MySwitch = $true
)
...
}
Good:
function Get-Something
{
param
(
[Switch]
$MySwitch
)
...
}
Bad:
Good:
Bad:
Good:
Bad:
Good:
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.
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
(
...
)
...
}
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
}
Bad:
Good:
Bad:
Good:
Bad:
Good:
Bad:
Good:
Bad:
Good:
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
}
}
Bad:
Good:
Bad:
Good:
Bad:
Good:
Bad:
Good:
Bad:
Good:
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.
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
.
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.
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)
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.
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'
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
}
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 $_
}
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 $_
}
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 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'
}
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'
}
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'
}
}
}