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.

Table of Contents

Indentation

Use four spaces per indentation level.

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)

Braces

The opening brace of a block is placed on the same line as its corresponding statement or declaration. For example:

if ($MyVar -gt 10) {
    ....
}

Variable Naming

For this project PascalCase will be used for variable names:

# Yes
$VariableName

# No
$variableName
$variable_name

Spacing

Trailing spaces

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.

Spaces around parameters and operators

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)

Spaces around special characters

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.

Avoid using semicolons (;) at the end of each line.

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
}  

Functions

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) {
    ...  
}

Advanced Functions

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
    }
}

Specify an OutputType attribute if the advanced function returns

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>")]

When a ParameterSetName is used in any of the parameters, always provide a

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
)

Documenting and Comments

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

Block comments

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
  #>

Inline comments

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
}

Documentation comments

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.

Location

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.

Put Details in the Notes

If you want to provide detailed explanations about how your tool works, use the Notes section for that.

Describe The Function

Every script function command should have at least a short statement describing it's function. That is the Synopsis.

Document Each Parameter

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.

Provide Usage Examples

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 ... #>
}

Write comment-based help

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.

Naming Conventions

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.

Use the full name of each command.

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

Use full parameter names.

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

Use full, explicit paths when possible.

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")

Avoid the use of ~ to represent the home folder.

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
⚠️ **GitHub.com Fallback** ⚠️