This document is still in-progress.
- General
- Calling Functions
- Use Named Parameters Instead of Positional Parameters
- Avoid Cmdlet Aliases
- Avoid Invoke-Expression
- Use the Force Parameter with Calls to ShouldContinue
- Avoid the WMI Cmdlets
- Avoid Write-Host
- Avoid ConvertTo-SecureString with AsPlainText
- Assign Function Results to Variables Rather Than Extensive Inline Calls
- 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
Using hardcoded computer names exposes sentitive 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 editted 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'
Writ-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
Call cmdlets using named parameters instead of positional parameters. Named parameters help other developers who are unfamiliar with your code to better understand your code.
Bad:
Get-ChildItem C:\Documents *.md
Good:
Get-ChildItem -Path C:\Documents -Filter *.md
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 { @('.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 Writ-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 retrive
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 retrive 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: