diff --git a/instructions/powershell.instructions.md b/instructions/powershell.instructions.md index 83be180b0..bf7ec4422 100644 --- a/instructions/powershell.instructions.md +++ b/instructions/powershell.instructions.md @@ -30,11 +30,11 @@ safe, and maintainable scripts. It aligns with Microsoft’s PowerShell cmdlet d - **Alias Avoidance:** - Use full cmdlet names - - Avoid using aliases in scripts (e.g., use Get-ChildItem instead of gci) + - Avoid using aliases in scripts (e.g., use `Get-ChildItem` instead of `gci`) - Document any custom aliases - Use full parameter names -### Example +### Example - Naming Conventions ```powershell function Get-UserProfile { @@ -49,6 +49,9 @@ function Get-UserProfile { ) process { + $outputString = "Searching for: '$($Username)'" + Write-Verbose -Message $outputString + Write-Verbose -Message "Profile type: $ProfileType" # Logic here } } @@ -75,12 +78,14 @@ function Get-UserProfile { - Enable tab completion where possible - **Switch Parameters:** - - Use [switch] for boolean flags - - Avoid $true/$false parameters - - Default to $false when omitted - - Use clear action names + - **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 +### Example - Parameter Design ```powershell function Set-ResourceConfiguration { @@ -93,16 +98,24 @@ function Set-ResourceConfiguration { [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 { - # Logic here + # Use .IsPresent to check switch state + if ($Quiet.IsPresent) { + Write-Verbose "Quiet mode enabled" + } } } ``` @@ -133,7 +146,7 @@ function Set-ResourceConfiguration { - Return modified/created object with `-PassThru` - Use verbose/warning for status updates -### Example +### Example - Pipeline and Output ```powershell function Update-ResourceStatus { @@ -163,7 +176,7 @@ function Update-ResourceStatus { Name = $Name Status = $Status LastUpdated = $timestamp - UpdatedBy = $env:USERNAME + UpdatedBy = "$($env:USERNAME)" } # Only output if PassThru is specified @@ -209,69 +222,32 @@ function Update-ResourceStatus { - Support automation scenarios - Document all required inputs -### Example +### Example - Error Handling and Safety ```powershell -function Remove-UserAccount { - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] +function Remove-CacheFiles { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')] param( - [Parameter(Mandatory, ValueFromPipeline)] - [ValidateNotNullOrEmpty()] - [string]$Username, - - [Parameter()] - [switch]$Force + [Parameter(Mandatory)] + [string]$Path ) - begin { - Write-Verbose 'Starting user account removal process' - $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 - } - - # Confirmation - $shouldProcessMessage = "Remove user account '$Username'" - if ($Force -or $PSCmdlet.ShouldProcess($Username, $shouldProcessMessage)) { - 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) + try { + $files = Get-ChildItem -Path $Path -Filter "*.cache" -ErrorAction Stop + + # Demonstrates WhatIf support + if ($PSCmdlet.ShouldProcess($Path, 'Remove cache files')) { + $files | Remove-Item -Force + Write-Verbose "Removed $($files.Count) cache files from $Path" } - } - - end { - Write-Verbose 'User account removal process completed' + } catch { + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $_.Exception, + 'RemovalFailed', + [System.Management.Automation.ErrorCategory]::NotSpecified, + $Path + ) + $PSCmdlet.WriteError($errorRecord) } } ``` @@ -307,50 +283,76 @@ function Remove-UserAccount { - Use `ForEach-Object` instead of `%` - Use `Get-ChildItem` instead of `ls` or `dir` +--- + ## Full Example: End-to-End Cmdlet Pattern ```powershell -function New-Resource { - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] +function Remove-UserAccount { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( - [Parameter(Mandatory = $true, - ValueFromPipeline = $true, - ValueFromPipelineByPropertyName = $true)] + [Parameter(Mandatory, ValueFromPipeline)] [ValidateNotNullOrEmpty()] - [string]$Name, + [string]$Username, [Parameter()] - [ValidateSet('Development', 'Production')] - [string]$Environment = 'Development' + [switch]$Force ) begin { - Write-Verbose 'Starting resource creation process' + Write-Verbose 'Starting user account removal process' + $ErrorActionPreference = $currentErrorActionValue + $ErrorActionPreference = 'Stop' } process { try { - if ($PSCmdlet.ShouldProcess($Name, 'Create new resource')) { - # Resource creation logic here - Write-Output ([PSCustomObject]@{ - Name = $Name - Environment = $Environment - Created = Get-Date - }) + # 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 with Force parameter + if ($PSCmdlet.ShouldProcess($Username, "Remove user account")) { + # ShouldContinue for additional confirmation when -Force is not used + 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, - 'ResourceCreationFailed', + 'UnexpectedError', [System.Management.Automation.ErrorCategory]::NotSpecified, - $Name + $Username ) $PSCmdlet.ThrowTerminatingError($errorRecord) } } end { - Write-Verbose 'Completed resource creation process' + Write-Verbose 'User account removal process completed' + # Set ErrorActionPreference back to the value it had + $ErrorActionPreference = $currentErrorActionValue } } ```