| applyTo | **/*.ps1,**/*.psm1 |
|---|---|
| description | PowerShell cmdlet and scripting best practices based on Microsoft guidelines |
This guide provides PowerShell-specific instructions to help GitHub Copilot generate idiomatic, safe, and maintainable scripts. It aligns with Microsoft’s PowerShell cmdlet development guidelines.
-
Verb-Noun Format:
- Use approved PowerShell verbs (Get-Verb)
- Use singular nouns
- PascalCase for both verb and noun
- Avoid special characters and spaces
-
Parameter Names:
- Use PascalCase
- Choose clear, descriptive names
- Use singular form unless always multiple
- Follow PowerShell standard names
-
Variable Names:
- Use PascalCase for public variables
- Use camelCase for private variables
- Avoid abbreviations
- Use meaningful names
-
Alias Avoidance:
- Use full cmdlet names
- Avoid using aliases in scripts (e.g., use
Get-ChildIteminstead ofgci) - Document any custom aliases
- Use full parameter names
function Get-UserProfile {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Username,
[Parameter()]
[ValidateSet('Basic', 'Detailed')]
[string]$ProfileType = 'Basic'
)
process {
$outputString = "Searching for: '$($Username)'"
Write-Verbose -Message $outputString
Write-Verbose -Message "Profile type: $ProfileType"
# Logic here
}
}-
Standard Parameters:
- Use common parameter names (
Path,Name,Force) - Follow built-in cmdlet conventions
- Use aliases for specialized terms
- Document parameter purpose
- Use common parameter names (
-
Parameter Names:
- Use singular form unless always multiple
- Choose clear, descriptive names
- Follow PowerShell conventions
- Use PascalCase formatting
-
Type Selection:
- Use common .NET types
- Implement proper validation
- Consider ValidateSet for limited options
- Enable tab completion where possible
-
Switch Parameters:
- ALWAYS use
[switch]for boolean flags, never[bool] - NEVER use
[bool]$Parameteror assign default values - Switch parameters default to
$falsewhen omitted - Use clear, action-oriented names
- Test presence with
.IsPresent - Using
$true/$falsein parameter attributes (e.g.,Mandatory = $true) is acceptable
- ALWAYS use
function Set-ResourceConfiguration {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Name,
[Parameter()]
[ValidateSet('Dev', 'Test', 'Prod')]
[string]$Environment = 'Dev',
# ✔️ CORRECT: Use `[switch]` with no default value
[Parameter()]
[switch]$Force,
# ❌ WRONG: Shows incorrect default assignment, however this is correct syntax (requires `[switch]` cast).
[Parameter()]
[switch]$Quiet = [switch]$true,
[Parameter()]
[ValidateNotNullOrEmpty()]
[string[]]$Tags
)
process {
# Use .IsPresent to check switch state
if ($Quiet.IsPresent) {
Write-Verbose "Quiet mode enabled"
}
}
}-
Pipeline Input:
- Use
ValueFromPipelinefor direct object input - Use
ValueFromPipelineByPropertyNamefor property mapping - Implement Begin/Process/End blocks for pipeline handling
- Document pipeline input requirements
- Use
-
Output Objects:
- Return rich objects, not formatted text
- Use PSCustomObject for structured data
- Avoid Write-Host for data output
- Enable downstream cmdlet processing
-
Pipeline Streaming:
- Output one object at a time
- Use process block for streaming
- Avoid collecting large arrays
- Enable immediate processing
-
PassThru Pattern:
- Default to no output for action cmdlets
- Implement
-PassThruswitch for object return - Return modified/created object with
-PassThru - Use verbose/warning for status updates
function Update-ResourceStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[string]$Name,
[Parameter(Mandatory)]
[ValidateSet('Active', 'Inactive', 'Maintenance')]
[string]$Status,
[Parameter()]
[switch]$PassThru
)
begin {
Write-Verbose 'Starting resource status update process'
$timestamp = Get-Date
}
process {
# Process each resource individually
Write-Verbose "Processing resource: $Name"
$resource = [PSCustomObject]@{
Name = $Name
Status = $Status
LastUpdated = $timestamp
UpdatedBy = "$($env:USERNAME)"
}
# Only output if PassThru is specified
if ($PassThru.IsPresent) {
Write-Output $resource
}
}
end {
Write-Verbose 'Resource status update process completed'
}
}-
ShouldProcess Implementation:
- Use
[CmdletBinding(SupportsShouldProcess = $true)] - Set appropriate
ConfirmImpactlevel - Call
$PSCmdlet.ShouldProcess()as close the the changes action - Use
$PSCmdlet.ShouldContinue()for additional confirmations
- Use
-
Message Streams:
Write-Verbosefor operational details with-VerboseWrite-Warningfor warning conditionsWrite-Errorfor non-terminating errorsthrowfor terminating errors- Avoid
Write-Hostexcept for user interface text
-
Error Handling Pattern:
- Use try/catch blocks for error management
- Set appropriate ErrorAction preferences
- Return meaningful error messages
- Use ErrorVariable when needed
- Include proper terminating vs non-terminating error handling
- In advanced functions with
[CmdletBinding()], prefer$PSCmdlet.WriteError()overWrite-Error - In advanced functions with
[CmdletBinding()], prefer$PSCmdlet.ThrowTerminatingError()overthrow - Construct proper ErrorRecord objects with category, target, and exception details
-
Non-Interactive Design:
- Accept input via parameters
- Avoid
Read-Hostin scripts - Support automation scenarios
- Document all required inputs
function Remove-CacheFiles {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory)]
[string]$Path
)
try {
$files = Get-ChildItem -Path $Path -Filter "*.cache" -ErrorAction Stop
# Demonstrates WhatIf support
if ($PSCmdlet.ShouldProcess($Path, 'Remove cache files')) {
$files | Remove-Item -Force -ErrorAction Stop
Write-Verbose "Removed $($files.Count) cache files from $Path"
}
} catch {
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$_.Exception,
'RemovalFailed',
[System.Management.Automation.ErrorCategory]::NotSpecified,
$Path
)
$PSCmdlet.WriteError($errorRecord)
}
}-
Comment-Based Help: Include comment-based help for any public-facing function or cmdlet. Inside the function, add a
<# ... #>help comment with at least:.SYNOPSISBrief description.DESCRIPTIONDetailed explanation.EXAMPLEsections with practical usage.PARAMETERdescriptions.OUTPUTSType of output returned.NOTESAdditional information
-
Consistent Formatting:
- Follow consistent PowerShell style
- Use proper indentation (4 spaces recommended)
- Opening braces on same line as statement
- Closing braces on new line
- Use line breaks after pipeline operators
- PascalCase for function and parameter names
- Avoid unnecessary whitespace
-
Pipeline Support:
- Implement Begin/Process/End blocks for pipeline functions
- Use ValueFromPipeline where appropriate
- Support pipeline input by property name
- Return proper objects, not formatted text
-
Avoid Aliases: Use full cmdlet names and parameters
- Avoid using aliases in scripts (e.g., use Get-ChildItem instead of gci); aliases are acceptable for interactive shell use.
- Use
Where-Objectinstead of?orwhere - Use
ForEach-Objectinstead of% - Use
Get-ChildIteminstead oflsordir
function Remove-UserAccount {
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[ValidateNotNullOrEmpty()]
[string]$Username,
[Parameter()]
[switch]$Force
)
begin {
Write-Verbose 'Starting user account removal process'
$currentErrorActionValue = $ErrorActionPreference
$ErrorActionPreference = 'Stop'
}
process {
try {
# Validation
if (-not (Test-UserExists -Username $Username)) {
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
[System.Exception]::new("User account '$Username' not found"),
'UserNotFound',
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
$Username
)
$PSCmdlet.WriteError($errorRecord)
return
}
# ShouldProcess enables -WhatIf and -Confirm support
if ($PSCmdlet.ShouldProcess($Username, "Remove user account")) {
# ShouldContinue provides an additional confirmation prompt for high-impact operations
# This prompt is bypassed when -Force is specified
if ($Force -or $PSCmdlet.ShouldContinue("Are you sure you want to remove '$Username'?", "Confirm Removal")) {
Write-Verbose "Removing user account: $Username"
# Main operation
Remove-ADUser -Identity $Username -ErrorAction Stop
Write-Warning "User account '$Username' has been removed"
}
}
} catch [Microsoft.ActiveDirectory.Management.ADException] {
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$_.Exception,
'ActiveDirectoryError',
[System.Management.Automation.ErrorCategory]::NotSpecified,
$Username
)
$PSCmdlet.ThrowTerminatingError($errorRecord)
} catch {
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$_.Exception,
'UnexpectedError',
[System.Management.Automation.ErrorCategory]::NotSpecified,
$Username
)
$PSCmdlet.ThrowTerminatingError($errorRecord)
}
}
end {
Write-Verbose 'User account removal process completed'
# Set ErrorActionPreference back to the value it had
$ErrorActionPreference = $currentErrorActionValue
}
}