|
| 1 | +# dbatools PowerShell Style Guide for Claude Code |
| 2 | + |
| 3 | +This style guide provides coding standards for dbatools PowerShell development to ensure consistency, readability, and maintainability across the project. |
| 4 | + |
| 5 | +## CRITICAL COMMAND SYNTAX RULES |
| 6 | + |
| 7 | +### NO BACKTICKS - ALWAYS USE SPLATS |
| 8 | + |
| 9 | +**ABSOLUTE RULE**: NEVER suggest or use backticks (`) for line continuation. Backticks are an anti-pattern in modern PowerShell development. |
| 10 | + |
| 11 | +### PARAMETER ATTRIBUTES - NO `= $true` SYNTAX |
| 12 | + |
| 13 | +**MODERN RULE**: Do NOT use `Mandatory = $true` or similar boolean attribute assignments. Boolean attributes do not require explicit value assignment in modern PowerShell. |
| 14 | + |
| 15 | +```powershell |
| 16 | +# CORRECT - Modern attribute syntax (no = $true) |
| 17 | +param( |
| 18 | + [Parameter(Mandatory)] |
| 19 | + [string]$SqlInstance, |
| 20 | +
|
| 21 | + [Parameter(ValueFromPipeline)] |
| 22 | + [object[]]$InputObject, |
| 23 | +
|
| 24 | + [switch]$EnableException |
| 25 | +) |
| 26 | +
|
| 27 | +# WRONG - Outdated PSv2 syntax (no longer needed) |
| 28 | +param( |
| 29 | + [Parameter(Mandatory = $true)] |
| 30 | + [string]$SqlInstance, |
| 31 | +
|
| 32 | + [Parameter(ValueFromPipeline = $true)] |
| 33 | + [object[]]$InputObject |
| 34 | +) |
| 35 | +``` |
| 36 | + |
| 37 | +**Guidelines:** |
| 38 | +- Use `[Parameter(Mandatory)]` not `[Parameter(Mandatory = $true)]` |
| 39 | +- Use `[switch]` for boolean flags, not `[bool]` parameters |
| 40 | +- Keep non-boolean attributes with values: `[Parameter(ValueFromPipelineByPropertyName = "Name")]` |
| 41 | + |
| 42 | +### SPLAT USAGE REQUIREMENT |
| 43 | + |
| 44 | +**USE SPLATS ONLY FOR 3+ PARAMETERS** |
| 45 | + |
| 46 | +- **1-2 parameters**: Use direct parameter syntax |
| 47 | +- **3+ parameters**: Use splatted hashtables with `$splat<Purpose>` naming |
| 48 | + |
| 49 | +```powershell |
| 50 | +# CORRECT - 2 parameters, direct syntax |
| 51 | +$database = Get-DbaDatabase -SqlInstance $instance -Name "master" |
| 52 | +
|
| 53 | +# CORRECT - 5 parameters, must use splat |
| 54 | +$splatConnection = @{ |
| 55 | + SqlInstance = $instance |
| 56 | + SqlCredential = $TestConfig.SqlCredential |
| 57 | + Database = $dbName |
| 58 | + EnableException = $true |
| 59 | + Confirm = $false |
| 60 | +} |
| 61 | +$result = New-DbaDatabase @splatConnection |
| 62 | +
|
| 63 | +# WRONG - 3+ parameters without splat |
| 64 | +Get-DbaDatabase -SqlInstance $instance -Database $db -EnableException $true -WarningAction SilentlyContinue |
| 65 | +
|
| 66 | +# WRONG - Using backticks for continuation |
| 67 | +Get-DbaDatabase -SqlInstance $instance ` |
| 68 | + -Database $db ` |
| 69 | + -EnableException $true |
| 70 | +
|
| 71 | +# WRONG - Generic $splat without purpose |
| 72 | +$splat = @{ |
| 73 | + SqlInstance = $instance |
| 74 | + Database = $db |
| 75 | + Confirm = $false |
| 76 | +} |
| 77 | +``` |
| 78 | + |
| 79 | +## COMMENT PRESERVATION REQUIREMENT |
| 80 | + |
| 81 | +**ABSOLUTE MANDATE**: ALL COMMENTS MUST BE PRESERVED EXACTLY as they appear in the original code. This includes: |
| 82 | +- Development notes and temporary comments |
| 83 | +- End-of-file comments |
| 84 | +- CI/CD system comments (especially AppVeyor) |
| 85 | +- Seemingly unrelated comments |
| 86 | +- Any comment that appears to be a note or reminder |
| 87 | +- Do not delete anything that says `#$TestConfig.instance...` or similar metadata |
| 88 | + |
| 89 | +**NO EXCEPTIONS** - Every single comment must remain intact in its original location and format. |
| 90 | + |
| 91 | +## STRING AND QUOTE STANDARDS |
| 92 | + |
| 93 | +- **Always use double quotes** for strings (SQL Server module standard) |
| 94 | +- Properly escape quotes when needed |
| 95 | +- Convert all single quotes to double quotes for string literals |
| 96 | +- Remove unnecessary quotes from parameter values |
| 97 | + |
| 98 | +```powershell |
| 99 | +# CORRECT |
| 100 | +$database = "master" |
| 101 | +$query = "SELECT * FROM sys.databases" |
| 102 | +$message = "Database `"$dbName`" created successfully" |
| 103 | +
|
| 104 | +# WRONG |
| 105 | +$database = 'master' |
| 106 | +$query = 'SELECT * FROM sys.databases' |
| 107 | +``` |
| 108 | + |
| 109 | +## HASHTABLE ALIGNMENT (MANDATORY) |
| 110 | + |
| 111 | +**CRITICAL FORMATTING REQUIREMENT**: ALL hashtable assignments must be perfectly aligned using spaces: |
| 112 | + |
| 113 | +```powershell |
| 114 | +# REQUIRED FORMAT - Aligned = signs |
| 115 | +$splatConnection = @{ |
| 116 | + SqlInstance = $TestConfig.instance2 |
| 117 | + SqlCredential = $TestConfig.SqlCredential |
| 118 | + Database = $dbName |
| 119 | + EnableException = $true |
| 120 | + Confirm = $false |
| 121 | +} |
| 122 | +
|
| 123 | +# FORBIDDEN - Misaligned hashtables |
| 124 | +$splat = @{ |
| 125 | + SqlInstance = $instance |
| 126 | + Database = $db |
| 127 | + EnableException = $true |
| 128 | +} |
| 129 | +``` |
| 130 | + |
| 131 | +The equals signs must line up vertically to create clean, professional-looking code. |
| 132 | + |
| 133 | +## VARIABLE NAMING CONVENTIONS |
| 134 | + |
| 135 | +- Use `$splat<Purpose>` for 3+ parameters (never plain `$splat`) |
| 136 | +- Use direct parameters for 1-2 parameters |
| 137 | +- Create unique variable names across all scopes to prevent collisions |
| 138 | +- Be descriptive with variable names to indicate their purpose |
| 139 | + |
| 140 | +```powershell |
| 141 | +# GOOD - descriptive splat names with aligned formatting |
| 142 | +$splatPrimary = @{ |
| 143 | + Primary = $TestConfig.instance3 |
| 144 | + Name = $primaryAgName |
| 145 | + ClusterType = "None" |
| 146 | + FailoverMode = "Manual" |
| 147 | + Certificate = "dbatoolsci_AGCert" |
| 148 | + Confirm = $false |
| 149 | +} |
| 150 | +
|
| 151 | +$splatReplica = @{ |
| 152 | + Secondary = $TestConfig.instance2 |
| 153 | + Name = $replicaAgName |
| 154 | + ClusterType = "None" |
| 155 | + Confirm = $false |
| 156 | +} |
| 157 | +
|
| 158 | +# Direct parameters for 1-2 parameters |
| 159 | +$ag = Get-DbaLogin -SqlInstance $instance -Login $loginName |
| 160 | +
|
| 161 | +# WRONG - Generic splat name |
| 162 | +$splat = @{ |
| 163 | + Primary = $TestConfig.instance3 |
| 164 | + Name = $agName |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### Unique Names Across Scopes |
| 169 | + |
| 170 | +Use unique, descriptive variable names across scopes to avoid collisions: |
| 171 | + |
| 172 | +```powershell |
| 173 | +Describe $CommandName { |
| 174 | + BeforeAll { |
| 175 | + $primaryInstanceName = "instance3" |
| 176 | + $splatPrimary = @{ |
| 177 | + SqlInstance = $primaryInstanceName |
| 178 | + Database = "testdb" |
| 179 | + } |
| 180 | + } |
| 181 | +
|
| 182 | + Context "Specific scenario" { |
| 183 | + BeforeAll { |
| 184 | + # Different variable name - not $primaryInstanceName again |
| 185 | + $secondaryInstanceName = "instance2" |
| 186 | + $splatSecondary = @{ |
| 187 | + SqlInstance = $secondaryInstanceName |
| 188 | + Database = "testdb" |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +## ARRAY FORMATTING |
| 196 | + |
| 197 | +Multi-line arrays must be formatted consistently: |
| 198 | + |
| 199 | +```powershell |
| 200 | +$expectedParameters = @( |
| 201 | + "SqlInstance", |
| 202 | + "SqlCredential", |
| 203 | + "Database", |
| 204 | + "EnableException" |
| 205 | +) |
| 206 | +
|
| 207 | +# Multi-line hashtable arrays |
| 208 | +$instances = @( |
| 209 | + @{ |
| 210 | + Name = "instance1" |
| 211 | + Version = "2019" |
| 212 | + }, |
| 213 | + @{ |
| 214 | + Name = "instance2" |
| 215 | + Version = "2022" |
| 216 | + } |
| 217 | +) |
| 218 | +``` |
| 219 | + |
| 220 | +## WHERE-OBJECT USAGE |
| 221 | + |
| 222 | +Prefer direct property comparison for simple filters: |
| 223 | + |
| 224 | +```powershell |
| 225 | +# Preferred - direct property comparison |
| 226 | +$master = $databases | Where-Object Name -eq "master" |
| 227 | +$systemDbs = $databases | Where-Object Name -in "master", "model", "msdb", "tempdb" |
| 228 | +
|
| 229 | +# Required - script block for complex filtering only |
| 230 | +$hasParameters = (Get-Command $CommandName).Parameters.Values.Name | Where-Object { $PSItem -notin ("WhatIf", "Confirm") } |
| 231 | +``` |
| 232 | + |
| 233 | +## FORMATTING RULES |
| 234 | + |
| 235 | +- Apply OTBS (One True Brace Style) formatting to all code blocks |
| 236 | +- No trailing spaces anywhere |
| 237 | +- Use `$results.Status.Count` for accurate counting in dbatools context |
| 238 | +- Preserve all original parameter names exactly as written |
| 239 | +- 4-space indentation for consistency |
| 240 | + |
| 241 | +```powershell |
| 242 | +# CORRECT - OTBS style |
| 243 | +if ($condition) { |
| 244 | + $result = Get-DbaDatabase -SqlInstance $instance |
| 245 | +} else { |
| 246 | + $result = $null |
| 247 | +} |
| 248 | +
|
| 249 | +# CORRECT - Foreach with OTBS |
| 250 | +foreach ($instance in $instances) { |
| 251 | + $splatQuery = @{ |
| 252 | + SqlInstance = $instance |
| 253 | + Query = "SELECT @@VERSION" |
| 254 | + } |
| 255 | + $null = Invoke-DbaQuery @splatQuery |
| 256 | +} |
| 257 | +``` |
| 258 | + |
| 259 | +## TEMPORARY FILES AND RESOURCE MANAGEMENT |
| 260 | + |
| 261 | +- Create temporary test files/directories with unique names using `Get-Random` |
| 262 | +- Always clean up temporary resources with `-ErrorAction SilentlyContinue` |
| 263 | +- Track all resources created for cleanup |
| 264 | + |
| 265 | +```powershell |
| 266 | +BeforeAll { |
| 267 | + # Create unique temp path for this test run |
| 268 | + $backupPath = "$($TestConfig.Temp)\$CommandName-$(Get-Random)" |
| 269 | + $null = New-Item -Path $backupPath -ItemType Directory |
| 270 | + $filesToRemove = @() |
| 271 | +} |
| 272 | +
|
| 273 | +AfterAll { |
| 274 | + # Always clean up temp files |
| 275 | + Remove-Item -Path $backupPath -Recurse -ErrorAction SilentlyContinue |
| 276 | + Remove-Item -Path $filesToRemove -ErrorAction SilentlyContinue |
| 277 | +} |
| 278 | +``` |
| 279 | + |
| 280 | +## DBATOOLS-SPECIFIC CONVENTIONS |
| 281 | + |
| 282 | +### Parameter Validation Pattern |
| 283 | + |
| 284 | +```powershell |
| 285 | +Context "Parameter validation" { |
| 286 | + It "Should have the expected parameters" { |
| 287 | + $hasParameters = (Get-Command $CommandName).Parameters.Values.Name | Where-Object { $PSItem -notin ("WhatIf", "Confirm") } |
| 288 | + $expectedParameters = @( |
| 289 | + "SqlInstance", |
| 290 | + "SqlCredential", |
| 291 | + "Database", |
| 292 | + "EnableException" |
| 293 | + ) |
| 294 | + Compare-Object -ReferenceObject $expectedParameters -DifferenceObject $hasParameters | Should -BeNullOrEmpty |
| 295 | + } |
| 296 | +} |
| 297 | +``` |
| 298 | + |
| 299 | +### EnableException Handling |
| 300 | + |
| 301 | +For integration tests, use EnableException to ensure test setup/cleanup failures are detected: |
| 302 | + |
| 303 | +```powershell |
| 304 | +BeforeAll { |
| 305 | + # Set EnableException for setup to catch failures |
| 306 | + $PSDefaultParameterValues['*-Dba*:EnableException'] = $true |
| 307 | +
|
| 308 | + # Perform setup operations |
| 309 | + $null = New-DbaDatabase -SqlInstance $instance -Name $testDb |
| 310 | +
|
| 311 | + # Remove EnableException for actual test execution |
| 312 | + $PSDefaultParameterValues.Remove('*-Dba*:EnableException') |
| 313 | +} |
| 314 | +
|
| 315 | +AfterAll { |
| 316 | + # Re-enable for cleanup |
| 317 | + $PSDefaultParameterValues['*-Dba*:EnableException'] = $true |
| 318 | + $null = Remove-DbaDatabase -SqlInstance $instance -Database $testDb |
| 319 | +} |
| 320 | +``` |
| 321 | + |
| 322 | +## VERIFICATION CHECKLIST |
| 323 | + |
| 324 | +**Comment and Parameter Preservation:** |
| 325 | +- [ ] All comments preserved exactly as in original |
| 326 | +- [ ] Parameter names match original exactly without modification |
| 327 | +- [ ] No backticks used for line continuation |
| 328 | +- [ ] No `= $true` used in parameter attributes (use modern syntax) |
| 329 | +- [ ] Splats used only for 3+ parameters |
| 330 | + |
| 331 | +**Style Requirements:** |
| 332 | +- [ ] Double quotes used for all strings |
| 333 | +- [ ] **MANDATORY**: Hashtable assignments perfectly aligned |
| 334 | +- [ ] Splat variables use descriptive `$splat<Purpose>` format |
| 335 | +- [ ] Variable names are unique across scopes |
| 336 | +- [ ] OTBS formatting applied throughout |
| 337 | +- [ ] No trailing spaces anywhere |
| 338 | + |
| 339 | +**dbatools Patterns:** |
| 340 | +- [ ] EnableException handling correctly implemented |
| 341 | +- [ ] Parameter validation follows dbatools pattern |
| 342 | +- [ ] Where-Object conversions applied appropriately |
| 343 | +- [ ] Temporary resource cleanup implemented properly |
| 344 | +- [ ] Splat usage follows 3+ parameter rule strictly |
| 345 | + |
| 346 | +## SUMMARY |
| 347 | + |
| 348 | +The golden rules for dbatools code: |
| 349 | + |
| 350 | +1. **NEVER use backticks** - Use splats for 3+ parameters, direct syntax for 1-2 |
| 351 | +2. **NEVER use `= $true` in parameter attributes** - Use modern syntax: `[Parameter(Mandatory)]` not `[Parameter(Mandatory = $true)]` |
| 352 | +3. **ALWAYS align hashtables** - Equals signs must line up vertically |
| 353 | +4. **ALWAYS preserve comments** - Every comment stays exactly as written |
| 354 | +5. **ALWAYS use double quotes** - SQL Server module standard |
| 355 | +6. **ALWAYS use unique variable names** - Prevent scope collisions |
| 356 | +7. **ALWAYS use descriptive splatnames** - `$splatConnection`, not `$splat` |
0 commit comments