--- applyTo: '**/*.ps1,**/*.psm1' description: 'PowerShell cmdlet and scripting best practices based on Microsoft guidelines' --- # PowerShell Cmdlet Development 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. ## Naming Conventions - **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-ChildItem` instead of `gci`) - Document any custom aliases - Use full parameter names ### Example - Naming Conventions ```powershell 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 } } ``` ## Parameter Design - **Standard Parameters:** - Use common parameter names (`Path`, `Name`, `Force`) - Follow built-in cmdlet conventions - Use aliases for specialized terms - Document parameter purpose - **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]$Parameter` or assign default values - Switch parameters default to `$false` when omitted - Use clear, action-oriented names - Test presence with `.IsPresent` - Using `$true`/`$false` in parameter attributes (e.g., `Mandatory = $true`) is acceptable ### Example - Parameter Design ```powershell 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 and Output - **Pipeline Input:** - Use `ValueFromPipeline` for direct object input - Use `ValueFromPipelineByPropertyName` for property mapping - Implement Begin/Process/End blocks for pipeline handling - Document pipeline input requirements - **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 `-PassThru` switch for object return - Return modified/created object with `-PassThru` - Use verbose/warning for status updates ### Example - Pipeline and Output ```powershell 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' } } ``` ## Error Handling and Safety - **ShouldProcess Implementation:** - Use `[CmdletBinding(SupportsShouldProcess = $true)]` - Set appropriate `ConfirmImpact` level - Call `$PSCmdlet.ShouldProcess()` as close the the changes action - Use `$PSCmdlet.ShouldContinue()` for additional confirmations - **Message Streams:** - `Write-Verbose` for operational details with `-Verbose` - `Write-Warning` for warning conditions - `Write-Error` for non-terminating errors - `throw` for terminating errors - Avoid `Write-Host` except 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()` over `Write-Error` - In advanced functions with `[CmdletBinding()]`, prefer `$PSCmdlet.ThrowTerminatingError()` over `throw` - Construct proper ErrorRecord objects with category, target, and exception details - **Non-Interactive Design:** - Accept input via parameters - Avoid `Read-Host` in scripts - Support automation scenarios - Document all required inputs ### Example - Error Handling and Safety ```powershell 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) } } ``` ## Documentation and Style - **Comment-Based Help:** Include comment-based help for any public-facing function or cmdlet. Inside the function, add a `<# ... #>` help comment with at least: - `.SYNOPSIS` Brief description - `.DESCRIPTION` Detailed explanation - `.EXAMPLE` sections with practical usage - `.PARAMETER` descriptions - `.OUTPUTS` Type of output returned - `.NOTES` Additional 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-Object` instead of `?` or `where` - Use `ForEach-Object` instead of `%` - Use `Get-ChildItem` instead of `ls` or `dir` --- ## Full Example: End-to-End Cmdlet Pattern ```powershell 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 } } ```