SCRIPT LIBRARY · POWERSHELL
Check PowerShell Scripts for Syntax Errors Without Running Them
Use PowerShell's own parser to catch typos and broken brackets in a whole folder of scripts, before anything runs.
- What it does
- Parses every .ps1, .psm1 and .psd1 file you point it at and reports each syntax error with the file, line and column. Nothing gets executed.
- Requires
- PowerShell 7+ or Windows PowerShell 5.1
- No modules
- Permissions
- Read access to the files. That's it.
- Runs on
- Windows, macOS, Linux, CI runners
- Tested
- Not yet re-tested for this edition
You know the feeling. You tweak a script, hand it off or schedule it, and an hour later it falls over on line 212 because of a curly brace you deleted by accident. The script never even got to the part you changed.
PowerShell has a full parser built in, the same one it uses right before it runs anything, and you can call it directly. It reads the file, builds the syntax tree, and tells you what's broken. It doesn't run a single line, so it's safe to point at scripts that delete things, touch production, or need credentials you don't have handy.
This little wrapper runs that parser over a file or a whole folder and gives you back a clean list of problems. I run it before every commit now, and it's the first check in every pipeline I build.
<#
.SYNOPSIS
Checks PowerShell scripts for syntax errors without running a single line of them.
.DESCRIPTION
Uses PowerShell's own parser to read each .ps1, .psm1, or .psd1 file and report any
parse errors with the file, line, and column. Nothing is executed. Handy before you
commit, before you hand a script to someone else, or as a quick CI check.
.PARAMETER Path
One or more files or folders. Folders are searched for PowerShell files.
.PARAMETER Recurse
Search subfolders too.
.EXAMPLE
.\Test-ScriptSyntax.ps1 -Path .\scripts -Recurse
.EXAMPLE
Get-ChildItem *.ps1 | .\Test-ScriptSyntax.ps1
#>
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('FullName')]
[string[]]$Path = '.',
[switch]$Recurse
)
begin {
$extensions = '.ps1', '.psm1', '.psd1'
$checked = 0
$failed = 0
}
process {
foreach ($item in $Path) {
$files = if (Test-Path -LiteralPath $item -PathType Container) {
Get-ChildItem -LiteralPath $item -File -Recurse:$Recurse | Where-Object { $extensions -contains $_.Extension }
}
else {
Get-Item -LiteralPath $item
}
foreach ($file in $files) {
$tokens = $null
$errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errors)
$checked++
if ($errors.Count -eq 0) {
Write-Verbose "OK $($file.FullName)"
continue
}
$failed++
foreach ($err in $errors) {
[pscustomobject]@{
File = $file.Name
Line = $err.Extent.StartLineNumber
Column = $err.Extent.StartColumnNumber
Message = $err.Message
Path = $file.FullName
}
}
}
}
}
end {
$color = if ($failed) { 'Red' } else { 'Green' }
Write-Host ("Checked {0} file(s): {1} with errors." -f $checked, $failed) -ForegroundColor $color
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Path | string[] | . | Files or folders to check. Folders are searched for .ps1, .psm1 and .psd1 files. Also takes pipeline input, so you can pipe Get-ChildItem straight into it. |
-Recurse | switch | — | Look in subfolders too. |
Run it
Check everything in a repo, subfolders included.
.\Test-ScriptSyntax.ps1 -Path .\scripts -RecurseJust the scripts you changed today.
Get-ChildItem *.ps1 | Where-Object LastWriteTime -gt (Get-Date).Date | .\Test-ScriptSyntax.ps1Fail a CI job if anything's broken.
if (.\Test-ScriptSyntax.ps1 -Path . -Recurse) { exit 1 }See every file it looked at, not only the broken ones.
.\Test-ScriptSyntax.ps1 -Path .\scripts -Recurse -VerboseWhat you'll see
File Line Column Message
---- ---- ------ -------
Deploy.ps1 42 16 Missing closing '}' in statement block or type definition.
Settings.psd1 7 1 The hash literal was incomplete.
Checked 18 file(s): 2 with errors.
How it works
The heavy lifting is one line:
[System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$errors)
ParseFile hands back the syntax tree, a list of tokens and a list of parse errors. We only care about the errors. If that list is empty the file is fine, and if it isn't, each error already knows where it lives, down to the line and column.
Everything else is plumbing:
- Figure out what to check. If you pass a folder, it grabs every PowerShell file in it (and below it, with
-Recurse). If you pass a file, it checks just that file. - Parse each one. No dot-sourcing, no
Invoke-Expression, no running anything. That's the whole point. - Return objects, one per error. You get
File,Line,Column,Messageand the fullPath, so you can sort, filter or export them like anything else. - Print a summary at the end so you know it actually looked at something. A clean run that checked zero files is a very different thing from a clean run that checked fifty.
Because clean files return nothing, "no output" means "no problems." That's what makes the CI example work: if (...) is only true when there's at least one error.
Take it further
- Add it as a Git pre-commit hook. Have the hook call the script on staged
.ps1files and refuse the commit if anything comes back. You'll never push a broken brace again. - Pair it with PSScriptAnalyzer. Run this first because it's instant, then
Invoke-ScriptAnalyzerfor the style and best-practice checks. - Check scripts before a scheduled task runs them. A quick syntax check at the top of a runner script is a cheap way to fail loudly instead of halfway through.
Things that'll trip you up
- It catches syntax, not logic. A misspelled cmdlet name or a wrong parameter still parses fine, because PowerShell can't know what'll be installed when the script runs. For that, add PSScriptAnalyzer on top.
- One mistake can cause a pile of errors. A missing brace near the top can make the parser confused about everything after it. Fix the first error on the list and run it again before chasing the rest.
- Version differences are real. Syntax that's new in PowerShell 7, like the ternary operator or ??, won't parse in 5.1. Run the check with the same version that'll run the script.
- The summary line is Write-Host. That's on purpose. It shows up on screen but stays out of the pipeline, so the only thing the script returns is the errors themselves.