| Property | Value |
|---|---|
| Version | 1.2 |
| Status | Stable |
| Applies To | All Keldor PowerShell projects |
| Last Updated | 2026-07-16 |
This standard defines the preferred engineering, documentation, compatibility, security, and style conventions for Keldor PowerShell projects.
It inherits from the Keldor General Engineering Standard.
Keldor PowerShell code should be secure, predictable, discoverable, and useful in real administrative and enterprise environments.
PowerShell commands should feel like a coherent toolkit, not a pile of unrelated scripts wearing the same hoodie.
Commands should return structured objects by default. Formatting belongs to the caller, format views, or documentation examples.
Validate inputs, avoid unsafe dynamic execution, do not hardcode secrets, and minimize privilege requirements.
New commands should support Windows, macOS, and Linux unless the command is inherently platform-specific.
Windows PowerShell 5.1 and Microsoft-supported PowerShell 7 release lines beginning with 7.4 are supported. PowerShell 7.6 LTS is preferred for development, automation, and CI. Shared production code must retain the Windows PowerShell 5.1 parser baseline.
Public commands should include comment-based help, HelpUri, examples, and matching documentation pages.
Similar commands should use similar parameter names, aliases, output object shapes, error behavior, and documentation structure.
Commands should avoid hidden side effects. State-changing commands should support -WhatIf and -Confirm through ShouldProcess.
Commands should work well in the pipeline and return objects suitable for filtering, exporting, and further automation.
Supported production runtimes:
- Windows PowerShell 5.1 on Microsoft-supported Windows versions.
- Microsoft-supported PowerShell 7 release lines beginning with 7.4.
PowerShell 7.6 LTS is the preferred development, automation, formatting, documentation, and CI runtime. Retired PowerShell releases are not compatibility targets.
PowerShell source uses four spaces for each indentation level. Tabs are not permitted.
Keldor uses One True Brace Style (OTBS): opening braces remain on the same line as the associated statement, and
else, elseif, catch, and finally remain on the same line as the preceding closing brace.
function Get-KeldorThing {
[CmdletBinding()]
param()
begin {
$Items = @()
}
process {
$Items += Get-KeldorItem
}
end {
$Items
}
clean {
Remove-Variable -Name Items -ErrorAction SilentlyContinue
}
}Use clean only in code whose declared compatibility target supports it.
if ($Condition) {
Invoke-Something
} elseif ($OtherCondition) {
Invoke-OtherThing
} else {
Invoke-DefaultThing
}
switch ($Status) {
'Ready' {
Start-KeldorThing
}
default {
Write-Warning 'The Keldor thing is not ready.'
}
}
foreach ($Item in $Items) {
Write-Output $Item
}try {
Invoke-Something -ErrorAction Stop
} catch {
Write-Error -ErrorRecord $_
} finally {
Remove-Variable -Name TemporaryValue -ErrorAction SilentlyContinue
}$ActiveItems = $Items | Where-Object {
$_.IsEnabled
}
class KeldorThing {
[string]$Name
KeldorThing([string]$Name) {
$this.Name = $Name
}
}Classes are appropriate only when the module's compatibility target supports them.
Use one space around assignment and binary operators and after commas. Apply the same spacing to attribute arguments, hashtable entries, and named arguments where PowerShell syntax permits it.
[Parameter(Mandatory = $true, Position = 0)]
[Alias('Host', 'Computer')]
[string]$ComputerName
$Parameters = @{
Path = $Path
ErrorAction = 'Stop'
}The target maximum line length is 120 characters. Prefer syntax-aware wrapping over backticks.
Use splatting when a command has several parameters or becomes difficult to read:
$Parameters = @{
Path = $Path
Filter = '*.ps1'
Recurse = $true
ErrorAction = 'Stop'
}
Get-ChildItem @ParametersFor pipelines, place the pipe at the end of the preceding line and use one stage per continuation line:
Get-ChildItem -Path $Path -Recurse |
Where-Object { $_.Extension -eq '.ps1' } |
Sort-Object -Property FullNameWrap attributes using their parenthesized form:
function Get-KeldorConfiguration {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSAvoidGlobalVars',
'',
Justification = 'Required for compatibility with the module configuration loader.'
)]
[CmdletBinding()]
param()
}Wrap compound Boolean expressions with one logical condition per line:
if (
$null -ne $CommandInfo -and
$CommandInfo.Parameters.ContainsKey('PredictionSource') -and
$PSCmdlet.ShouldProcess($Target, $Operation)
) {
Invoke-Something
}Short continuations may be indented by four spaces when splatting would add unnecessary complexity. Avoid backticks unless no safer readable alternative exists.
Do not split strings in ways that change their value. Use here-strings for intentionally multiline content, format expressions where appropriate, or intermediate variables when they improve clarity. Preserve external-command argument ordering and quoting; wrapping must not change native argument-passing behavior.
URLs, HelpUri values, .LINK values, hashes, identifiers, and other indivisible literals may exceed 120 characters
when wrapping would harm correctness, usability, or copy-and-paste behavior.
| Feature | Windows PowerShell 5.1 | Supported PowerShell 7 | Guidance |
|---|---|---|---|
| Advanced functions | Yes | Yes | Use for public commands. |
| Classes | Yes | Yes | Use only when they improve a stable design. |
| Enums | Yes | Yes | Use only when they improve a stable design. |
[pscustomobject] and [ordered] |
Yes | Yes | Prefer for structured output. |
| CIM cmdlets | Yes | Platform-dependent | Prefer for Windows management when behaviorally safe. |
| WMI cmdlets | Yes | Windows compatibility varies | Retain only for remote, vendor-provider, or tested fallback needs. |
ForEach-Object -Parallel |
No | Yes | Do not use in shared module code. |
| Ternary and null-coalescing operators | No | Yes | Do not use in shared module code. |
using namespace |
Yes | Yes | Use cautiously because module class discovery has parse-time behavior. |
$IsWindows, $IsLinux, $IsMacOS |
No | Yes | Use the Keldor platform helper outside its bootstrap implementation. |
Use PascalCase module names with clear ownership or purpose.
Examples:
Keldor
Keldor.Build.PowerShell
Keldor.Build.Python
Public functions must use approved PowerShell verbs.
Get-VerbUse singular nouns unless the noun is naturally plural.
Good:
Get-KeldorProject
Test-KeldorRepository
Invoke-KeldorBuildAvoid vague names:
Do-Stuff
Run-Thing
Fix-IssueUse descriptive PascalCase variable names.
Good:
$ComputerName
$RegistryPath
$CurrentUser
$ConnectionStringAvoid unclear names except in very small loop scopes:
$Comp
$Reg
$temp
$xUse singular/plural intentionally.
$Computer
$Computers
$User
$Users
$Item
$ItemsBoolean variables should read naturally.
$IsAdmin
$IsInstalled
$HasAccess
$SupportsRemotingAvoid vague boolean names such as $Admin or $Installed.
Use this layout for most public functions:
function Get-KeldorThing {
<#
.SYNOPSIS
Gets a Keldor thing.
.DESCRIPTION
Gets a Keldor thing from the specified path.
.PARAMETER Path
Specifies the path to inspect.
.EXAMPLE
Get-KeldorThing -Path .
Gets a Keldor thing from the current directory.
.OUTPUTS
Keldor.Thing
.LINK
https://docs.keldor.dev/powershell/keldor/Get-KeldorThing
#>
[CmdletBinding(HelpUri = 'https://docs.keldor.dev/powershell/keldor/Get-KeldorThing')]
param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]$Path
)
process {
# Function logic goes here.
}
}Keldor uses a lean default comment-help format.
Required for public functions:
.SYNOPSIS.DESCRIPTION.PARAMETER.EXAMPLE.OUTPUTS.LINK
Optional only when useful:
.INPUTS.NOTES
Discouraged by default:
.COMPONENT.FUNCTIONALITY.ROLE.FORWARDHELPTARGETNAME.FORWARDHELPCATEGORY.REMOTEHELPRUNSPACE.EXTERNALHELP- Author
- Created date
- Last modified date
- Version
- Requirements
Git tracks authorship and history. Module manifests track module metadata. #Requires belongs at the top of scripts when needed, not buried in help text.
Public functions should include a HelpUri in [CmdletBinding()].
The HelpUri value should match the .LINK value.
Format:
https://docs.keldor.dev/powershell/keldor/<FunctionName>
Example:
function Get-KeldorThing {
[CmdletBinding(HelpUri = 'https://docs.keldor.dev/powershell/keldor/Get-KeldorThing')]
param()
}Do not create empty lifecycle blocks.
Use process for most functions.
Use begin only for initialization.
Use end only for cleanup, aggregation, or final output.
Use clean only for cleanup that must run when a pipeline is stopped early, and only when the compatibility target
supports the clean block.
Avoid:
begin {}
process {}
end {}Use [CmdletBinding()] for public functions.
Use SupportsShouldProcess when a command makes a meaningful external or persistent state change. This includes
creating or registering resources, modifying configuration, installing software, copying or restoring data, joining
systems, mounting resources, repairing or updating systems, restarting or stopping services, and synchronization that
changes either side. The decision is based on behavior, not the cmdlet verb; read-only Import, Save, or Copy
commands do not need ShouldProcess merely because of their names.
function Remove-KeldorThing {
[CmdletBinding(
SupportsShouldProcess = $true,
ConfirmImpact = 'Medium',
HelpUri = 'https://docs.keldor.dev/powershell/keldor/Remove-KeldorThing'
)]
param()
}Read-only commands should not implement ShouldProcess.
Use ConfirmImpact = 'Low' for routine reversible changes, Medium for meaningful changes that deserve user
awareness, and High for destructive or difficult-to-reverse operations. Reserve High for operations where the
default confirmation prompt is warranted.
Call ShouldProcess immediately before the state change. Use a concise target that identifies the affected resource
and an action phrase that describes the operation:
if ($PSCmdlet.ShouldProcess($Path, 'Remove Keldor thing')) {
Remove-Item -Path $Path -Force
}Under -WhatIf, the state-changing operation must not run. Under -Confirm, each meaningful operation should provide
an understandable prompt. Avoid prompts for discovery, validation, and other read-only work.
When calling a nested command that supports ShouldProcess, prevent duplicate prompts by guarding the operation in the
outer command and using -Confirm:$false for the nested call. Forward -WhatIf only when the outer command intentionally
delegates the decision instead of making its own ShouldProcess call.
Use this parameter attribute order:
[Parameter()][Alias()], when applicable- Validation attributes
- Type
- Variable name
- Default value, if needed
Example:
[Parameter(Mandatory = $true, Position = 0)]
[ValidateNotNullOrEmpty()]
[string]$PathPrefer named parameter options with spaces around =:
Mandatory = $trueAvoid:
Mandatory=$true| Canonical Parameter | Standard Aliases | Notes |
|---|---|---|
ComputerName |
HostName, DnsHostName, Name |
Reduce aliases when they conflict with command semantics. Preserve established legacy aliases for compatibility. |
Credential |
Cred |
Use only when credentials are accepted. |
InputObject |
Input |
Use for pipeline-friendly object input. |
Path |
None by default | Add LiteralPath separately when literal behavior is needed. |
For new fleet and remote functions, follow the canonical pattern in the Input & Output Standard. Existing local-default commands may retain an optional parameter and legacy aliases when changing them would be breaking.
[Parameter(
Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true
)]
[Alias('HostName', 'DnsHostName', 'Name')]
[ValidateNotNullOrEmpty()]
[string[]]$ComputerNameSupport pipeline input when it makes the command more useful and predictable.
Use ValueFromPipeline for full object input.
Use ValueFromPipelineByPropertyName when matching common property names such as ComputerName, Name, or Path.
Pipeline-aware functions should generally use a process block.
Fleet and infrastructure commands are governed by the Fleet and Infrastructure Contract, including canonical target parameters, parameter sets, concurrency, timeout and retry semantics, normalized result types, per-target failure behavior, and compatibility requirements.
Commands should return objects, not formatted text.
Use PascalCase property names.
Use consistent property names across commands.
Prefer ComputerName over mixing Computer, Comp, and Host in output objects.
Defined public fleet contracts must use an intentional property order and stable Keldor PSTypeName. Keep native
values as native types and put units in property names rather than display strings. See the
Input & Output Standard for the normative
rules and result contracts.
Use this order when practical:
- Identity:
ComputerName,Name,Id - Classification:
Type,Category,Source - Configuration: paths, settings, options
- Measurements: counts, sizes, durations
- State:
Status,IsEnabled,IsRunning,IsInstalled - Diagnostics: event-specific timestamps,
Error,Warning,Message
Example:
[pscustomobject]@{
PSTypeName = 'Keldor.Network.Interface'
ComputerName = $ComputerName
Name = $Name
Id = $Id
IPAddress = $IPAddress
MacAddress = $MacAddress
Status = $Status
IsUp = $IsUp
CheckedAt = Get-Date
}Boolean output properties should read naturally as Boolean values: IsEnabled, IsAvailable, IsInstalled,
HasChanges, or CanRestart. Existing public properties must be preserved when renaming would break consumers. Add the
canonical property alongside the legacy property, test both, document the compatibility property, and remove it only in
a planned major-version migration.
Timestamp properties must identify the recorded event. Prefer names such as CreatedAt, UpdatedAt, CheckedAt,
DiscoveredAt, InstalledAt, StartedAt, CompletedAt, and LastSeenAt; avoid Timestamp, Date, and Time.
Return [datetime] values by default or [datetimeoffset] when timezone and transport semantics matter. Do not format a
timestamp as a string unless the public output contract explicitly requires a string.
Use [pscustomobject] and [ordered] for new structured output. Preserve older object construction only when a tested
public contract requires incremental migration.
Use terminating errors when execution cannot safely continue.
Use non-terminating errors when processing can continue for other input objects.
Avoid empty catch blocks.
If returning fallback objects from catch, include enough diagnostic context to explain the failure.
| Mechanism | Use When |
|---|---|
Write-Verbose |
Diagnostic information useful during normal troubleshooting. |
Write-Debug |
Developer-focused troubleshooting details. |
Write-Information |
User-facing informational stream output. |
Write-Warning |
Recoverable concern that may affect results. |
Write-Error |
Non-terminating error for one item while continuing. |
throw |
Terminating error when execution cannot safely continue. |
Avoid Write-Host in reusable functions unless direct host output is the purpose of the command.
Do not log secrets, tokens, passwords, connection strings, private keys, or sensitive environment details.
Security-sensitive code should:
- Validate input paths, names, filters, registry keys, and command arguments
- Avoid
Invoke-Expression - Avoid shell injection
- Quote external process arguments carefully
- Use least privilege
- Avoid storing secrets in files or source code
- Avoid writing sensitive data to logs
- Prefer explicit allow lists over broad matching when practical
New commands should be cross-platform unless inherently platform-specific.
Use:
Join-Pathinstead of string-building paths.
Avoid hardcoded path separators.
Place platform-specific public commands under the matching platform folder:
Public/Common
Public/Windows
Public/macOS
Public/Linux
Windows-only commands should avoid pretending to be cross-platform. Say what they are. No trench coat required.
Prefer foreach over ForEach-Object for in-memory collections when readability and performance matter.
Avoid += on arrays inside loops for large collections.
Cache expensive lookups.
Stream output when practical instead of accumulating large arrays.
Filter as close to the data source as practical.
Every public command should eventually have Pester tests.
Tests should cover:
- Successful operation
- Invalid input
- Missing dependencies
- Platform assumptions
- Security-sensitive behavior
- Error paths
Every public cmdlet should eventually have:
- Comment-based help
HelpUri- Function-specific documentation page
- At least one example
- Pester tests
- Changelog entry for behavior changes
PowerShell repositories should include:
.editorconfig.gitattributes.gitignore.markdownlint.json.markdownlintignoreREADME.mdCHANGELOG.mdCONTRIBUTING.mdSECURITY.mdLICENSEPSScriptAnalyzerSettings.psd1.github/workflows/
Run the checked-in formatter configuration with:
Get-ChildItem -Path ./src -Recurse -File -Include *.ps1, *.psm1, *.psd1 |
ForEach-Object {
$Content = Get-Content -LiteralPath $_.FullName -Raw
$Formatted = Invoke-Formatter -ScriptDefinition $Content -Settings ./PSScriptAnalyzerSettings.psd1
Set-Content -LiteralPath $_.FullName -Value $Formatted -NoNewline
}Do not remove public commands without a transition plan.
When behavior changes:
- Document the replacement.
- Add warnings when appropriate.
- Update docs and changelog.
- Remove only in a major version when practical.
Existing legacy functions do not need to be rewritten only for style.
When touching a legacy function, modernize nearby code when practical:
- Remove author/date metadata from
.NOTES - Normalize
HelpUriand.LINK - Normalize spacing around
= - Prefer lowercase
param - Remove excessive blank lines
- Replace end-of-block comments like
}#foreachwhen they add no value - Improve output property consistency
- Add
ShouldProcessto modifying functions
Reviewers should ask:
- Does the function use an approved verb?
- Does it use the Keldor function layout?
- Does it include lean comment-based help?
- Does
HelpUrimatch.LINK? - Are parameters named and ordered consistently?
- Are inputs validated?
- Does it return objects instead of formatted text?
- Are output properties consistent with similar commands?
- Does a modifying command support
ShouldProcess? - Are secrets avoided?
- Are platform assumptions clear?
- Are tests included or planned?
- Is documentation updated?
- Is the changelog updated for behavior changes?
Keldor.Build.PowerShell should eventually provide automated checks such as:
Test-KeldorEngineeringStandardTest-KeldorRepositoryTest-KeldorDocumentationTest-KeldorCommentHelpTest-KeldorHelpUriTest-KeldorNamingTest-KeldorCompatibilityTest-KeldorSecurityTest-KeldorPerformanceTest-KeldorStyle
The standard should become enforceable through tooling, not just inspirational wall art.