PowerShell Authoring Rules
Overview
This skill is the mandatory rulebook for authoring PowerShell on Windows. Whenever a task
requires writing or running a PowerShell command, one-liner, or script, consult this skill
first and follow its rules exactly. It encodes the syntax rules, hard constraints, and
verification checklist that prevent the most common PowerShell authoring failures: Bash-isms
leaking into PowerShell, silent backtick breakage, --% parser shutdown, mangled quoting,
lost exit codes, and 5.1-incompatible syntax.
The content of this skill is self-contained — everything needed to write correct PowerShell
is in this file and the referenced references/ file. Do not fall back on Bash habits.
When To Use
Apply this skill whenever any of the following is true:
- A PowerShell command, one-liner, pipeline, or
.ps1/.psm1script is being authored. - PowerShell code is being reviewed, debugged, or corrected.
- A Windows shell operation is requested: 批处理文件、批量重命名、读取/写入文件、改环境变量、
启动原生程序、管道过滤、生成 JSON/CSV。 - A decision must be made between PowerShell and another runtime (Python, Node) for a task.
- Any question arises about PowerShell quoting, escaping, operators, or version compatibility.
Mandatory Workflow
Follow these steps in order for every PowerShell authoring request. Do not skip Step 1.
- Consult the rules. Re-read the rule sections below (and
references/extended-examples.md
when the snippet is non-trivial) before emitting code. - Choose the execution mode. Select a simple one-liner, a persistent session, or a
different runtime per "Choosing the Execution Mode". - Draft the code. Apply every rule and honor every hard constraint in this file.
- Self-check. Run the "Pre-Output Self-Check Checklist" against the draft. If the draft is
substantial, additionally runscripts/check_ps_syntax.pyon it and fix every reported
violation before output. Never emit code that fails the checklist.
Choosing the Execution Mode
- Run a simple PowerShell command directly as a one-liner.
- Consider Python for complex or stateful tasks. Do not force a long stateful workflow into a
single PowerShell one-liner. - For multi-step interactive work, start a persistent session with
interactive=True, then
reuse the same tool withtask_id=<id>to send input and read output in one step. - Use
wait_for_patternto wait for a prompt in a persistent session. TaskOutputremains available as a fallback for listing and monitoring tasks.- Send
exitto close a persistent session.
1. Cmdlets and Naming
- Cmdlets use
Verb-Nounnames. Core vocabulary:
Get-ChildItem(list files),Get-Content(read file),Set-Location(cd),
Copy-Item,Move-Item,Remove-Item,New-Item,Select-String(grep),
Get-Command,Get-Help. - Alias names (
ls,cat,cd,rm,cp,mv) resolve to cmdlets but differ from their
Bash namesakes. Prefer the fullVerb-Nounform in scripts for clarity; aliases are
acceptable in throwaway one-liners only when the mapping is exact.
2. Splatting and Exit Codes
- Splat cmdlet parameters with
@{}:
powershell
$p = @{ LiteralPath = $f; Destination = $d }
Copy-Item @p
$LASTEXITCODEis for native commands only — it does not report cmdlet success.
Use$?or the cmdlet's own error stream for cmdlets.
3. Pipelines and Objects
- The pipeline
|passes .NET objects, not plain text. Shape the stream with
Where-Object,Select-Object,ForEach-Object,Sort-Object,Measure-Object. foreach (...) { }is a statement, not an expression — it cannot be piped directly.
Either assign the result first, or useForEach-Object:
```powershell
# Wrong — a foreach statement cannot be piped
foreach ($x in $list) { $x.Name } | Sort-Object
# Right — assign first
$names = foreach ($x in $list) { $x.Name }
$names | Sort-Object
# Right — use the pipeline cmdlet
$list | ForEach-Object { $_.Name } | Sort-Object
```
4. Operators
- Comparison:
-eq-ne-gt-ge-lt-le,-like(wildcard),-match(regex),
-contains(collection membership),-replace(regex replace). - Logical:
-and-or-not(or!). - Comparison operators are case-insensitive by default; prefix with
c/i(-ceq,-imatch)
when case sensitivity matters.
5. Command Chaining and Continuation
- Chain commands with
;(always run next) or&&/||(PowerShell 7+: run next only on
success / only on failure). On Windows PowerShell 5.1,&&and||are syntax errors —
see "Windows PowerShell 5.1 Baseline". - Avoid backtick continuation
`— a trailing space after the backtick silently breaks
the line. Use@()arrays or let the line break naturally after pipes, commas, or operators:
``powershell
# Avoid
Get-ChildItem -Path C:\Temp
-Recurse
# Prefer — continue after a pipe, comma, or operator
Get-ChildItem -Path C:\Temp |
Where-Object { $_.Length -gt 1MB }
```
- Avoid
--%(Stop-Parsing) — it disables parsing for the remainder of the line. Use it
only for fixed literal native commands that need raw argument passthrough.
6. Strings and Quoting
'single quotes'are literal."double quotes"expand$variablesand$(subexpressions).- Use
${name}for variable boundaries and$($obj.prop)for properties:
powershell
"${name}_suffix"
"length is $($obj.Length)"
- Avoid Bash-style escaping such as
"\"q\"". Use'"q"', or backtick-escape inside a
double-quoted string:""q"".
7. Here-Strings, Literal Text, and JSON
- Here-strings:
@'...'@(literal) or@"..."@(expanded). - The opening delimiter must be last on its line; the closing delimiter must be alone at
the start of a line. - No Bash heredocs:
python - <<'PY'is invalid in PowerShell. - Prefer
ConvertTo-Jsonover manual JSON escaping:
powershell
$obj = [ordered]@{ name = 'demo'; count = 3 }
$json = $obj | ConvertTo-Json -Depth 5
Set-Content -LiteralPath out.json -Value $json -Encoding utf8
8. Native Executables and Argument Passing
- Native arguments:
& $exe @argList. - Do not use
$args— it is an automatic variable and will not behave as an argument list. - An omitted argument,
'', and$nullare distinct values; do not treat them as equivalent. - Capture
$LASTEXITCODEimmediately after the native call — capture it before piping,
because$LASTEXITCODEmay not be set once native output is piped into a cmdlet:
powershell
& $exe @argList
$code = $LASTEXITCODE
if ($code -ne 0) { throw "exe failed with exit code $code" }
$LASTEXITCODEholds the exit code of the last native command;$?is$trueif the last
command succeeded.
9. Environment Variables
- Read/write session-scoped values with
$env:NAME. - Persist values with
[Environment]::SetEnvironmentVariable('NAME', 'value', 'Scope'). - Resolution priority: Process > User > Machine.
- List them with
Get-ChildItem Env:. - Append to PATH instead of overwriting, and check for duplicates first:
powershell
if ($env:PATH -notlike '*C:\tools*') { $env:PATH += ';C:\tools' }
- Do not use
%NAME%inside PowerShell — that is cmd.exe syntax and is taken literally. - Child process changes do not propagate back to the parent process.
10. Parameter Binding
- Parameter value expressions must be parenthesized:
-Index (100..120), not-Index 100..120. - Use
-LiteralPath(not-Path) whenever a path may contain wildcard characters[ ] * ?.
11. Windows PowerShell 5.1 Baseline
Assume Windows PowerShell 5.1 for compatibility unless the target is explicitly PowerShell 7+.
On 5.1 the following are unavailable or syntax errors:
&&and||chaining — use;plus explicitif ($?) { ... }, or-ErrorAction.- Ternary
?:, null-coalescing??, null-conditional?.. -ParallelonForEach-Object,Get-Error,Test-Json, and other 7+-only cmdlets.
If 7+ is confirmed available, these constructs become legal — but prefer the 5.1-compatible
form when the edition is unknown.
12. Pre-Output Self-Check Checklist
Verify every item before emitting PowerShell code. Fix any failure, then re-check.
- [ ] No
%NAME%environment-variable syntax. - [ ] No Bash heredoc (
<<'PY',<<EOF). - [ ] No backtick line continuation; line breaks occur after pipes, commas, or operators.
- [ ] No
--%unless the command is a fixed literal native invocation. - [ ] No piped
foreach (...) { }statement. - [ ] No
$argsused as a native argument list. - [ ] No Bash-style escaping (
"\"q\""); use'"q"'or backtick escaping. - [ ]
$($obj.prop)used for properties inside double-quoted strings;${name}used for
variable boundaries. - [ ]
$LASTEXITCODEcaptured immediately, before piping native output. - [ ] Range expressions passed as parameters are parenthesized.
- [ ]
-LiteralPathused for user- or data-supplied paths. - [ ]
&&/||/?:/??/?.absent unless PowerShell 7+ is confirmed. - [ ]
@'...'@/@"..."@delimiters: opener last on its line, closer alone at line start. - [ ] JSON produced via
ConvertTo-Json, not manual string escaping. - [ ]
scripts/check_ps_syntax.pyrun on any non-trivial snippet, with zero violations.
Resources
references/extended-examples.md
Extended correct/incorrect worked examples covering file operations, pipeline shaping,
environment variables, native executables, JSON generation, and 5.1-vs-7 compatibility.
Load this file when writing non-trivial scripts or when debugging a syntax failure.
scripts/check_ps_syntax.py
Static rule checker for PowerShell snippets: a deterministic enforcement pass over the rules in
Sections 5–11 plus the extended common Bash-ism set. Run it on any non-trivial snippet before
emitting code.
"C:\Users\Seele Vollerei\.workbuddy\binaries\python\versions\3.13.12\python.exe" \
"<skill-dir>/scripts/check_ps_syntax.py" --file draft.ps1
Also accepts --text "<snippet>" and stdin. Exit code 0 means clean, 1 means violations,
2 means usage/IO error. --ps-version 7 allows 7+-only syntax; --strict fails on warnings;
--json emits machine-readable output.
Rule index:
| Rule | Detects |
|---|---|
| PS001 | %NAME% cmd.exe variable syntax (also inside quoted strings) |
| PS002 | Bash heredoc <<EOF / <<'PY' |
| PS003 | --% Stop-Parsing |
| PS004 | $args used as a native argument list |
| PS005 | Bash-style escaping (backslash-quote) |
| PS006 | CMD-style 2>nul redirect |
| PS007 | Bash export VAR=value |
| PS008 | Bash if [ ... ] test syntax |
| PS009 | Unix tools (grep/sed/awk) invoked as cmdlets |
| PS010 | Unparenthesized range passed as a parameter value |
| PS011 | Backtick line continuation, incl. the trailing-space break |
| PS012 | Piped foreach (...) { } statement |
| PS013 | $LASTEXITCODE read after native output was piped |
| PS014 | Here-string opener with content on the same line |
| PS015 | Indented here-string closing delimiter |
| PS016 | -Path given a literal containing [ ] * ? |
| PS017 | Set-Content/Out-File without -Encoding (5.1 ANSI default) |
| PS101–105 | && \|\| ?? ?. ?: used on the 5.1 baseline |