Style Guide for Poject - PSGumshoe/PSGumshoe GitHub Wiki
So as to make sure that all code follows a single format ensuring readability and follow best practices this document defines a basic style guide to be followed for contributors. Components for this style guide where take from PowerShell Style Guide and Best Practices and adapted for this project.
- Indentation
- Braces
- Variable Naming
- Spacing
-
Functions
- Advanced Functions
- Specify an OutputType attribute if the advanced function returns
- When a ParameterSetName is used in any of the parameters, always provide a
- When using advanced functions or scripts with CmdletBinding attribute avoid validating parameters in the body of the script when possible and use parameter validation attributes instead.
- Documenting and Comments
- Naming Conventions
This is what PowerShell ISE does and understands, and it's the default for most code editors. As always, existing projects may have different standards, but for public code, please stick to 4 spaces, and the rest of us will try to do the same.
The 4-space rule is optional for continuation lines. Hanging indents (when indenting a wrapped command which was too long) may be indented more than one indentation level, or may even be indented an odd number of spaces to line up with a method call or parameter block.
# This is ok
$MyObj.GetData(
$Param1,
$Param2,
$Param3,
$Param4
)
# This is better
$MyObj.GetData($Param1,
$Param2,
$Param3,
$Param4)
The opening brace of a block is placed on the same line as its corresponding statement or declaration. For example:
if ($MyVar -gt 10) {
....
}
For this project PascalCase will be used for variable names:
# Yes
$VariableName
# No
$variableName
$variable_name
Lines should not have trailing whitespace. Extra spaces result in future edits where the only change is a space being added or removed, making the analysis of the changes more difficult for no reason.
You should us a single space around parameter names and operators, including comparison operators and math and assignment operators, even when the spaces are not necessary for PowerShell to correctly parse the code.
One notable exception is when using semi-colons to pass values to switch parameters:
# Do not write:
$variable=Get-Content $FilePath -Wai:($ReadCount-gt0) -First($ReadCount*5)
# Instead write:
$variable = Get-Content -Path $FilePath -Wait:($ReadCount -gt 0) -TotalCount ($ReadCount * 5)
White-space is (mostly) irrelevant to PowerShell, but its proper use is the key to writing easily readable code.
Use a single space after commas and semicolons, and around pairs of curly braces.
Avoid extra spaces inside parenthesis or square braces.
Nested expressions $( ... )
and script blocks { ... }
should have a single space inside them to make code stand out and be more readable.
Nested expressions $( ... )
and variable delimiters ${...}
inside strings do not need spaces outside, since that would become a part of the string.
PowerShell will not complain about extra semicolons, but they are unecessary, and get in the way when code is being edited or copy-pasted. They also result in extra do-nothing edits in source control when someone finally decides to delete them.
They are also unecessary when declaring hashtables if you are already putting each element on it's own line:
# This is the preferred way to declare a hashtable if it must go past one line:
$Options = @{
Margin = 2
Padding = 2
FontSize = 24
}
Avoid using the return
keyword in your functions. Just place the object variable on its own.
When declaring simple functions leave a space between the function name and the parameters.
function MyFunction ($param1, $param2) {
...
}
For Advanced Functions and scripts use the format of <verb- for
naming. For a list of approved verbs the cmdlet Get-Verb
will list
them. On the noun side it can be composed of more than one joined word
using Camel Case and only singular nouns.
In Advanced Functions do not use the keyword return
to return an object.
In Advanced Functions you return objects inside the Process {}
block
and not in Begin {}
or End {}
since it defeats the advantage of the pipeline.
# Bad
function Get-USCitizenCapability {
[CmdletBinding()]
[OutputType([psobject])]
param (
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[int16]
$Age
)
process {
$Capabilities = @{
MilitaryService = $false
DrinkAlcohol = $false
Vote = $false
}
if ($Age -ge 18)
{
$Capabilities['MilitaryService'] = $true
$Capabilities['Vote'] = $true
}
$Obj = New-Object -Property $Capabilities -TypeName psobject
}
end { return $Obj }
}
# Good
function Get-USCitizenCapability {
[CmdletBinding()]
[OutputType([psobject])]
param(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[int16]
$Age
)
process {
$Capabilities = @{
MilitaryService = $false
DrinkAlcohol = $false
Vote = $false
}
if ($Age -ge 18) {
$Capabilities['MilitaryService'] = $true
$Capabilities['Vote'] = $true
}
New-Object -Property $Capabilities -TypeName psobject
}
}
an object or collection of objects. If the function returns different object types depending on the parameter set provide one per parameter set.
[OutputType([<TypeLiteral>], ParameterSetName="<Name>")]
[OutputType("<TypeNameString>", ParameterSetName="<Name>")]
DefaultParameterSetName in the CmdletBinding attribute.
function Get-User {
[CmdletBinding(DefaultParameterSetName="ID")]
[OutputType("System.Int32", ParameterSetName="ID")]
[OutputType([String], ParameterSetName="Name")]
param (
[parameter(Mandatory=$true, ParameterSetName="ID")]
[Int[]]
$UserID,
[parameter(Mandatory=$true, ParameterSetName="Name")]
[String[]]
$UserName
)
<# function body #>
}
When using advanced functions or scripts with CmdletBinding attribute avoid validating parameters in the body of the script when possible and use parameter validation attributes instead.
- AllowNull Validation Attribute
The AllowNull attribute allows the value of a mandatory parameter to be null ($null).
param (
[Parameter(Mandatory=$true)]
[AllowNull()]
[String]
$ComputerName
)
- AllowEmptyString Validation Attribute
The AllowEmptyString attribute allows the value of a mandatory parameter to be an empty string ("").
param (
[Parameter(Mandatory=$true)]
[AllowEmptyString()]
[String]
$ComputerName
)
- AllowEmptyCollection Validation Attribute
The AllowEmptyCollection attribute allows the value of a mandatory parameter to be an empty collection (@()).
param (
[Parameter(Mandatory=$true)]
[AllowEmptyCollection()]
[String[]]
$ComputerName
)
- ValidateCount Validation Attribute
The ValidateCount attribute specifies the minimum and maximum number of parameter values that a parameter accepts. Windows PowerShell generates an error if the number of parameter values in the command that calls the function is outside that range.
param (
[Parameter(Mandatory=$true)]
[ValidateCount(1,5)]
[String[]]
$ComputerName
)
- ValidateLength Validation Attribute
The ValidateLength attribute specifies the minimum and maximum number of characters in a parameter or variable value. Windows PowerShell generates an error if the length of a value specified for a parameter or a variable is outside of the range.
param (
[Parameter(Mandatory=$true)]
[ValidateLength(1,10)]
[String[]]
$ComputerName
)
- ValidatePattern Validation Attribute
The ValidatePattern attribute specifies a regular expression that is compared to the parameter or variable value. Windows PowerShell generates an error if the value does not match the regular expression pattern.
param (
[Parameter(Mandatory=$true)]
[ValidatePattern("[0-9][0-9][0-9][0-9]")]
[String[]]
$ComputerName
)
- ValidateRange Validation Attribute
The ValidateRange attribute specifies a numeric range for each parameter or variable value. Windows PowerShell generates an error if any value is outside that range.
param (
[Parameter(Mandatory=$true)]
[ValidateRange(0,10)]
[Int]
$Attempts
)
- ValidateScript Validation Attribute
The ValidateScript attribute specifies a script that is used to validate a parameter or variable value. Windows PowerShell pipes the value to the script, and generates an error if the script returns "false" or if the script throws an exception.
When you use the ValidateScript attribute, the value that is being validated is mapped to the $_ variable. You can use the $_ variable to refer to the value in the script.
param (
[Parameter()]
[ValidateScript({$_ -ge (get-date)})]
[DateTime]
$EventDate
)
- ValidateSet Attribute
The ValidateSet attribute specifies a set of valid values for a parameter or variable. Windows PowerShell generates an error if a parameter or variable value does not match a value in the set. In the following example, the value of the Detail parameter can only be "Low," "Average," or "High."
param (
[Parameter(Mandatory=$true)]
[ValidateSet("Low", "Average", "High")]
[String[]]
$Detail
)
- ValidateNotNull Validation Attribute
The ValidateNotNull attribute specifies that the parameter value cannot be null ($null). Windows PowerShell generates an error if the parameter value is null.
The ValidateNotNull attribute is designed to be used when the type of the parameter value is not specified or when the specified type will accept a value of Null. (If you specify a type that will not accept a null value, such as a string, the null value will be rejected without the ValidateNotNull attribute, because it does not match the specified type.)
param (
[Parameter(Mandatory=$true)]
[ValidateNotNull()]
$ID
)
- ValidateNotNullOrEmpty Validation Attribute
The ValidateNotNullOrEmpty attribute specifies that the parameter value cannot be null ($null) and cannot be an empty string (""). Windows PowerShell generates an error if the parameter is used in a function call, but its value is null, an empty string, or an empty array.
param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[String[]]
$UserName
)
Comments that contradict the code are worse than no comments. Always make a priority of keeping the comments up-to-date when the code changes!
Comments should be in English, and should be complete sentences. If the comment is short, the period at the end can be omitted.
Remember that comments should serve to your reasoning and decision-making, not attempt to explain what a command does. With the exception of regular expressions, well-written PowerShell can be pretty self-explanatory.
# Do not write:
# Increment Margin by 2
$Margin = $Margin + 2
# Maybe write:
# The rendering box obscures a couple of pixels.
$Margin = $Margin + 2
Don't go overboard with comments. Unless your code is particularly obscure, don't precede each line with a comment -- doing so breaks up the code and makes it harder to read. Instead, write a single block comment.
Block comments generally apply to some or all of the code which follows them, and are indented to the same level as that code. Each line should start with a # and a single space.
If the block is particularly long (as in the case of documentation text) it is recommended to use the <# ... #>
block comment syntax, but you should place the comment characters on their own lines, and indent the comment:
# Requiring a space makes things legible and prevents confusion.
# Writing comments one-per line makes them stand out more in the console.
<#
.Synopsis
Really long comment blocks are tedious to keep commented in single-line mode
.Description
Particularly when the comment must be frequently edited,
as with the help and documentation for a function or script
#>
Comments on the same line as a statement can be distracting, but when they don't state the obvious, and particularly when you have several short lines of code which need explaining, they can be useful.
They should be separated from the code statement by at least two spaces, and ideally, they should line up with any other inline comments in the same block of code.
$Options = @{
Margin = 2 # The rendering box obscures a couple of pixels.
Padding = 2 # We need space between the border and the text
FontSize = 24 # Keep this above 16 so it's readable in presentations
}
Comment-based help should be written in simple language.
You're not writing a thesis for your college Technical Writing class - you're writing something that describes how a function works. Avoid unecessarily large words, and keep your explanations short. You're not trying to impress anyone, and the only people who will ever read this are just trying to figure out how to use the function.
If you're writing in what is, for you, a foreign language, simpler words and simpler sentence structures are better, and more likely to make sense to a native reader.
Be complete, but be concise.
In order to ensure that the documentation stays with the function, documentation comments should be placed INSIDE the function, rather than above. To make it harder to forget to update them when changing a function, you should keep them at the top of the function, rather than at the bottom.
Of course, that's not to say that putting them elsewhere is wrong -- but this is easier to do, and harder to forget to update.
If you want to provide detailed explanations about how your tool works, use the Notes
section for that.
Every script function command should have at least a short statement describing it's function. That is the Synopsis
.
Each parameter should be documented. To make it easier to keep the comments synchronized with changes to the parameters, the parameter documentation comments may within the param
block, directly above each parameter.
It is also possible to write .Parameter
statements with the rest of the documentation comments, but they will be less likely to be left un-updated if you put them closer to the actual code they document.
Your help should always provide an example for each major use case. A 'usage example' is just an example of what you would type in to Powershell to run the script - you can even cut and paste one from the command line while you're testing your function.
function Test-Help {
<#
.Synopsis
An example function to display how help should be written
.Example
Get-Help -Name Test-Help
This shows the help for the example function
#>
[CmdletBinding()]
param(
# This parameter doesn't do anything.
# Aliases: MP
[Parameter(Mandatory=$true)]
[Alias("MP")]
[String]$MandatoryParameter
)
<# code here ... #>
}
You should always write comment-based help in your scripts and functions. At the moment we are not working on other language other than US English in the future once the project it is in a stable position we will work on updatable help and add multi language support.
Comment-based help is formatted as follows:
function get-example {
<#
.SYNOPSIS
A brief description of the function or script.
.DESCRIPTION
A longer description.
.PARAMETER FirstParameter
Description of each of the parameters
.PARAMETER SecondParameter
Description of each of the parameters
.INPUTS
Description of objects that can be piped to the script
.OUTPUTS
Description of objects that are output by the script
.EXAMPLE
Example of how to run the script
.LINK
Links to further documentation
.NOTES
Detail on what the script does, if this is needed
#>
Comment-based help is displayed when the user types help get-example
or get-example -?
, etc.
Your help should be helpful. That is, if you've written a tool called Get-LOBAppUser
, don't write help that merely says, "Gets LOB App Users." Duh.
Further information: You can get more on the use of comment-based help by typing help about_Comment_Based_Help
within Powershell.
In general, prefer the use of full explicit names for commands and parameters rather than aliases or short forms. There are tools Expand-Alias for fixing many, but not all of these issues.
Every PowerShell scripter learns the actual command names, but different people learn and use different aliases (e.g.: ls for Linux users, dir for DOS users, gci ...). In your shared scripts you should use the more universally known full command name. As a bonus, sites like GitHub will highlight commands properly when you use the full Verb-Noun name:
# Do not write:
gwmi -Class win32_service
# Instead write:
Get-WmiObject -Class Win32_Service
Because there are so many commands in PowerShell, it's impossible for every scripter to know every command. Therefore it's useful to be explicit about your parameter names for the sake of readers who may be unfamiliar with the command you're using. This will also help you avoid bugs if a future change to the command alters the parameter sets.
# Do not write:
Get-WmiObject win32_service name,state
# Instead write:
Get-WmiObject -Class win32_service -Property name,state
When writing scripts, it's really only safe to use ..
or .
in a path if you have previously explicitly set the location (within the script), and even then you should beware of using relative paths when calling .Net methods or legacy/native applications, because they will use the [Environment]::CurrentDirectory
rather than PowerShell's present working directory ($PWD
). Because checking for these types of errors is tedious (and because they are easy to over-look) it's best to avoid using relative paths altogether, and instead, base your paths off of $PSScriptRoot (the folder your script is in) when necessary.
# Do not write:
Get-Content .\README.md
# Especially do not write:
[System.IO.File]::ReadAllText(".\README.md")
# Instead write:
Get-Content (Join-Path $PSScriptRoot README.md)
# Or even use string concatenation:
[System.IO.File]::ReadAllText("$PSScriptRoot\README.md")
The meaning of ~ is unfortunately dependent on the "current" provider at the time of execution. This isn't really a style issue, but it's an important rule for code you intend to share anyway. Instead, use ${Env:UserProfile}
or (Get-PSProvider FileSystem).Home
...
PS C:\Windows\system32> cd ~
PS C:\Users\Name> cd HKCU:\Software
PS HKCU:\Software> cd ~
cd : Home location for this provider is not set. To set the home location, call "(get-psprovider 'Registry').Home = 'path'".
At line:1 char:1
+ cd ~
+ ~~~~
+ CategoryInfo : InvalidOperation: (:) [Set-Location], PSInvalidOperationException
+ FullyQualifiedErrorId : InvalidOperation,Microsoft.PowerShell.Commands.SetLocationCommand