PowerShell Script Writing Guide
This document provides guidelines for writing PowerShell scripts in a consistent and maintainable manner. It covers naming conventions, script structure, and best practices to ensure clarity and efficiency in your scripts.
Script Structure
PowerShell scripts should follow a clear and consistent structure to enhance readability and maintainability. A typical script structure includes the following elements:
Header Comments: Include a comment block at the beginning of the script to describe its purpose, author, and date. This helps others (and your future self) understand the script's intent quickly.
Param Block: If your script accepts parameters, define them in a
param
block at the beginning. This makes it clear what inputs the script expects.Function Definitions: Organize your code into functions to promote reusability and clarity. Each function should have a clear purpose and be documented with comments.
Main Script Logic: After defining functions, include the main logic of your script. This is where you call your functions and implement the core functionality.
Error Handling: Implement error handling throughout your script to manage exceptions gracefully. Use
try/catch
blocks where appropriate.Footer Comments: Include a comment block at the end of the script to summarize its functionality and any important notes.
By following this structure, you can create PowerShell scripts that are easier to read, understand, and maintain.
Code Formatting Standards
Allman Formatting Style
PowerShell scripts should follow the Allman formatting style (also known as BSD style) for consistent code structure and readability:
Brace Placement
- Opening braces on new line: Place opening braces
{
on a new line, aligned with the statement - Closing braces aligned: Closing braces
}
align with the opening statement - Consistent indentation: Use tabs for indentation (as specified in coding instructions)
# ✅ Correct Allman style
function Get-UserInformation
{
param(
[Parameter(Mandatory = $true)]
[string]$UserName
)
if ($UserName -eq "admin")
{
Write-Output "Administrator account detected"
}
else
{
Write-Output "Regular user account"
}
}
# ❌ Avoid - Inline braces
function Get-UserInformation {
param([string]$UserName)
if ($UserName -eq "admin") {
Write-Output "Administrator account detected"
} else {
Write-Output "Regular user account"
}
}
Control Structures
Apply Allman style to all control structures:
# If statements
if ($condition)
{
# Code block
}
elseif ($otherCondition)
{
# Code block
}
else
{
# Code block
}
# Loops
foreach ($item in $collection)
{
# Process item
}
while ($condition)
{
# Loop body
}
# Try-catch blocks
try
{
# Risky operation
}
catch
{
# Error handling
}
finally
{
# Cleanup
}
Benefits of Allman Style
- Visual clarity: Clear separation between statement and code block
- Easier debugging: Easier to set breakpoints on opening braces
- Consistent readability: Uniform appearance across all scripts
- Industry standard: Widely adopted in PowerShell community
Naming Conventions
When naming scripts, functions, and variables, adhere to the following conventions:
Use Descriptive Names: Choose names that clearly describe the purpose or functionality of the script, function, or variable. Avoid vague names like
Script1
orTempVar
.CamelCase for Functions and Variables: Use CamelCase for function names and variable names (e.g.,
Get-UserInfo
,$userName
).Prefix Script Names: Prefix script names with a category or module name to avoid naming conflicts (e.g.,
Networking_Get-IPAddress.ps1
).Use Verb-Noun Pairs for Functions: Name functions using a verb-noun pair to indicate their action (e.g.,
Get-User
,Set-Configuration
).Avoid Abbreviations: Use full words instead of abbreviations to enhance readability (e.g., use
Configuration
instead ofConfig
).Consistent Naming Patterns: Stick to a consistent naming pattern throughout your scripts to make them easier to understand and maintain.
By following these naming conventions, you can create PowerShell scripts that are more intuitive and easier to work with.
Step-by-Step Script Example
Let's walk through creating a complete PowerShell script that follows all the best practices outlined in this guide. We'll create a script that manages user accounts and demonstrates proper structure, formatting, and documentation.
Step 1: Create the Script Header
Start with comprehensive header comments that describe the script's purpose:
<#
.SYNOPSIS
Manages Active Directory user accounts with comprehensive validation and logging.
.DESCRIPTION
This script provides functionality to create, modify, and validate user accounts
in Active Directory. It includes proper error handling, logging, and follows
PowerShell best practices for enterprise environments.
.PARAMETER UserName
The username for the account to be managed.
.PARAMETER Action
The action to perform: Create, Modify, or Validate.
.PARAMETER LogPath
Path to the log file. Defaults to script directory.
.EXAMPLE
.\Manage-UserAccounts.ps1 -UserName "jdoe" -Action "Create"
Creates a new user account for John Doe.
.EXAMPLE
.\Manage-UserAccounts.ps1 -UserName "jsmith" -Action "Validate" -LogPath "C:\Logs"
Validates an existing user account and logs to specified path.
.NOTES
Author: Your Name
Date: $(Get-Date -Format 'yyyy-MM-dd')
Version: 1.0
Requires: ActiveDirectory module, appropriate permissions
.LINK
https://docs.microsoft.com/en-us/powershell/module/activedirectory/
#>
Step 2: Define Parameters with Validation
Use the [CmdletBinding()]
attribute and proper parameter validation:
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, HelpMessage = "Enter the username")]
[ValidateNotNullOrEmpty()]
[ValidateLength(3, 20)]
[string]$UserName,
[Parameter(Mandatory = $true)]
[ValidateSet("Create", "Modify", "Validate")]
[string]$Action,
[Parameter()]
[ValidateScript({
if (Test-Path (Split-Path $_ -Parent))
{
$true
}
else
{
throw "Directory does not exist: $(Split-Path $_ -Parent)"
}
})]
[string]$LogPath = "$PSScriptRoot\UserManagement.log"
)
Step 3: Create Helper Functions
Organize code into reusable functions with proper documentation:
function Write-LogMessage
{
<#
.SYNOPSIS
Writes timestamped messages to log file and console.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$Message,
[Parameter()]
[ValidateSet("Info", "Warning", "Error")]
[string]$Level = "Info"
)
$TimeStamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$LogEntry = "[$TimeStamp] [$Level] $Message"
# Write to console with appropriate stream
switch ($Level)
{
"Info" { Write-Host $LogEntry -ForegroundColor Green }
"Warning" { Write-Warning $LogEntry }
"Error" { Write-Error $LogEntry }
}
# Write to log file
try
{
Add-Content -Path $LogPath -Value $LogEntry -ErrorAction Stop
}
catch
{
Write-Warning "Failed to write to log file: $($_.Exception.Message)"
}
}
function Test-UserAccountExists
{
<#
.SYNOPSIS
Checks if a user account exists in Active Directory.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$UserName
)
try
{
$User = Get-ADUser -Identity $UserName -ErrorAction Stop
return $true
}
catch [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]
{
return $false
}
catch
{
Write-LogMessage -Message "Error checking user existence: $($_.Exception.Message)" -Level "Error"
throw
}
}
function New-UserAccount
{
<#
.SYNOPSIS
Creates a new Active Directory user account.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$UserName
)
Write-LogMessage -Message "Starting user creation process for: $UserName"
try
{
if (Test-UserAccountExists -UserName $UserName)
{
Write-LogMessage -Message "User already exists: $UserName" -Level "Warning"
return $false
}
$SecurePassword = ConvertTo-SecureString "TempPassword123!" -AsPlainText -Force
$UserParams = @{
Name = $UserName
SamAccountName = $UserName
UserPrincipalName = "$UserName@$env:USERDNSDOMAIN"
AccountPassword = $SecurePassword
Enabled = $true
ChangePasswordAtLogon = $true
}
New-ADUser @UserParams -ErrorAction Stop
Write-LogMessage -Message "Successfully created user: $UserName"
return $true
}
catch
{
Write-LogMessage -Message "Failed to create user $UserName`: $($_.Exception.Message)" -Level "Error"
throw
}
}
Step 4: Implement Main Script Logic
Use proper error handling and clear logic flow:
# Main script execution
Write-LogMessage -Message "Script started with Action: $Action, UserName: $UserName"
try
{
# Import required module
if (-not (Get-Module -Name ActiveDirectory -ListAvailable))
{
throw "ActiveDirectory module is not available. Please install RSAT tools."
}
Import-Module ActiveDirectory -ErrorAction Stop
Write-LogMessage -Message "ActiveDirectory module imported successfully"
# Execute based on action parameter
switch ($Action)
{
"Create"
{
$Result = New-UserAccount -UserName $UserName
if ($Result)
{
Write-LogMessage -Message "User creation completed successfully"
}
}
"Validate"
{
$Exists = Test-UserAccountExists -UserName $UserName
if ($Exists)
{
Write-LogMessage -Message "User validation successful: $UserName exists"
}
else
{
Write-LogMessage -Message "User validation failed: $UserName does not exist" -Level "Warning"
}
}
"Modify"
{
if (Test-UserAccountExists -UserName $UserName)
{
Write-LogMessage -Message "User modification functionality not yet implemented" -Level "Warning"
}
else
{
Write-LogMessage -Message "Cannot modify non-existent user: $UserName" -Level "Error"
}
}
}
}
catch
{
Write-LogMessage -Message "Script execution failed: $($_.Exception.Message)" -Level "Error"
exit 1
}
finally
{
Write-LogMessage -Message "Script execution completed"
}
Step 5: Add Footer Comments
Complete the script with summary information:
<#
.FOOTER
This script demonstrates PowerShell best practices including:
- Comprehensive parameter validation
- Proper error handling with try/catch blocks
- Allman formatting style for readability
- Detailed logging and user feedback
- Modular function design for reusability
- Complete comment-based help documentation
For production use, consider adding:
- Configuration file support
- More robust credential handling
- Additional user property management
- Integration with company policies
#>
Using PSScriptAnalyzer for Code Quality
What is PSScriptAnalyzer?
PSScriptAnalyzer is a static code analysis tool for PowerShell that checks scripts against best practices and coding standards. It helps identify potential issues, security problems, and style violations before deployment.
Installing PSScriptAnalyzer
# Install from PowerShell Gallery
Install-Module -Name PSScriptAnalyzer -Scope CurrentUser -Force
# Verify installation
Get-Module PSScriptAnalyzer -ListAvailable
Basic Usage
Analyzing a Single Script
# Analyze a script file
Invoke-ScriptAnalyzer -Path "C:\Scripts\MyScript.ps1"
# Get detailed output
Invoke-ScriptAnalyzer -Path "C:\Scripts\MyScript.ps1" -Severity @('Error', 'Warning', 'Information')
# Save results to file
Invoke-ScriptAnalyzer -Path "C:\Scripts\MyScript.ps1" | Out-File "C:\Reports\Analysis.txt"
Analyzing Multiple Scripts
# Analyze all scripts in a directory
Invoke-ScriptAnalyzer -Path "C:\Scripts\" -Recurse
# Analyze with specific rules
Invoke-ScriptAnalyzer -Path "C:\Scripts\" -IncludeRule @('PSAvoidUsingPlainTextForPassword', 'PSUseDeclaredVarsMoreThanAssignments')
# Exclude specific rules
Invoke-ScriptAnalyzer -Path "C:\Scripts\" -ExcludeRule @('PSAvoidUsingWriteHost')
Common Rules and Fixes
Security Rules
# ❌ Problematic - Plain text password
$Password = "MySecretPassword"
# ✅ Correct - Secure string
$SecurePassword = Read-Host -AsSecureString "Enter password"
$SecurePassword = ConvertTo-SecureString "Password" -AsPlainText -Force
# ❌ Problematic - Hardcoded credentials
$Credential = New-Object System.Management.Automation.PSCredential("user", "password")
# ✅ Correct - Proper credential handling
$Credential = Get-Credential
Performance Rules
# ❌ Problematic - Using Where-Object when not needed
Get-Process | Where-Object ProcessName -eq "notepad"
# ✅ Correct - Using specific parameters
Get-Process -Name "notepad"
# ❌ Problematic - Array addition in loop
$Results = @()
foreach ($item in $collection)
{
$Results += $item
}
# ✅ Correct - Using ArrayList or Generic List
$Results = [System.Collections.Generic.List[object]]@()
foreach ($item in $collection)
{
$Results.Add($item)
}
Best Practice Rules
# ❌ Problematic - Using aliases in scripts
gci | ? Name -like "*.txt" | % { $_.FullName }
# ✅ Correct - Using full cmdlet names
Get-ChildItem | Where-Object Name -like "*.txt" | ForEach-Object { $_.FullName }
# ❌ Problematic - Missing parameter validation
function Get-UserInfo($UserName) { }
# ✅ Correct - Proper parameter validation
function Get-UserInfo
{
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$UserName
)
}
Integration with Development Workflow
VS Code Integration
Add PSScriptAnalyzer settings to VS Code:
{
"powershell.scriptAnalysis.enable": true,
"powershell.scriptAnalysis.settingsPath": ".vscode/PSScriptAnalyzerSettings.psd1"
}
Custom Rules Configuration
Create a PSScriptAnalyzer settings file:
# PSScriptAnalyzerSettings.psd1
@{
Severity = @('Error', 'Warning')
IncludeRules = @(
'PSAvoidDefaultValueForMandatoryParameter',
'PSAvoidDefaultValueSwitchParameter',
'PSAvoidGlobalVars',
'PSAvoidUsingPlainTextForPassword',
'PSUseDeclaredVarsMoreThanAssignments'
)
ExcludeRules = @(
'PSAvoidUsingWriteHost' # Allow Write-Host for user interaction
)
}
Pre-commit Analysis
Create a PowerShell function for pre-commit checks:
function Test-ScriptQuality
{
param(
[Parameter(Mandatory = $true)]
[string]$ScriptPath
)
$Issues = Invoke-ScriptAnalyzer -Path $ScriptPath -Severity @('Error', 'Warning')
if ($Issues)
{
Write-Host "Script analysis found issues:" -ForegroundColor Red
$Issues | Format-Table -AutoSize
return $false
}
else
{
Write-Host "Script analysis passed!" -ForegroundColor Green
return $true
}
}
Best Practices
Comment Your Code: Use comments to explain complex logic or important decisions in your script. This helps others understand your thought process and makes it easier to maintain the code.
Use Consistent Indentation: Maintain consistent indentation throughout your script to improve readability. Use tabs for indentation as specified in the Allman formatting style.
Follow Allman Formatting: Use the Allman style with opening braces on new lines for better readability and debugging.
Avoid Hardcoding Values: Use variables or configuration files for values that may change, such as file paths or server names. This makes your script more flexible and easier to update.
Test Your Scripts: Before deploying scripts in a production environment, thoroughly test them in a safe environment. This helps catch errors and ensures the script behaves as expected.
Use Version Control: Store your scripts in a version control system (e.g., Git) to track changes, collaborate with others, and roll back to previous versions if needed.
Follow Security Best Practices: Be cautious with sensitive information, such as passwords or API keys. Use secure methods to handle credentials, such as the
Get-Credential
cmdlet or secure strings.Keep Scripts Modular: Break down large scripts into smaller, reusable functions or modules. This promotes code reuse and makes it easier to test and maintain individual components.
Use Verbose Output: Implement verbose output in your scripts to provide additional information during execution. This can be helpful for debugging and understanding script behavior.
Document Your Scripts: Maintain comprehensive comment-based help for your scripts, including usage instructions, parameter descriptions, and examples. This helps users understand how to use the script effectively.
Use PSScriptAnalyzer: Regularly analyze your scripts with PSScriptAnalyzer to identify potential issues, security problems, and style violations before deployment.
Implement Proper Error Handling: Use try/catch blocks for expected errors and provide meaningful error messages without exposing sensitive information.
Use Parameter Validation: Implement comprehensive parameter validation using validation attributes to ensure input data meets requirements.
Review and Refactor: Regularly review your scripts for opportunities to improve performance, readability, and maintainability. Refactor code as needed to keep it clean and efficient.
By adhering to these best practices, you can create PowerShell scripts that are robust, maintainable, and easy to understand.
Conclusion
In conclusion, following a structured approach to PowerShell scripting can greatly enhance the quality and maintainability of your scripts. By adhering to best practices, naming conventions, and a clear script structure, you can create scripts that are not only functional but also easy to read and understand. This ultimately leads to more efficient development and a smoother experience for both script authors and users.