My goal was to have a PowerShell function that removes any comment inside my code except help comments. Thankfully, with help from Chris Dent, I created a method that eliminates comments from any given file and makes it production ready. While it's part of my module builder PSPublishModule, I'm attaching it if you want to try it on your own.
function Remove-Comments {
<#
.SYNOPSIS
Remove comments from PowerShell file
.DESCRIPTION
Remove comments from PowerShell file and optionally remove empty lines
By default comments in param block are not removed
By default comments before param block are not removed
.PARAMETER SourceFilePath
File path to the source file
.PARAMETER Content
Content of the file
.PARAMETER DestinationFilePath
File path to the destination file. If not provided, the content will be returned
.PARAMETER RemoveEmptyLines
Remove empty lines if more than one empty line is found
.PARAMETER RemoveAllEmptyLines
Remove all empty lines from the content
.PARAMETER RemoveCommentsInParamBlock
Remove comments in param block. By default comments in param block are not removed
.PARAMETER RemoveCommentsBeforeParamBlock
Remove comments before param block. By default comments before param block are not removed
.EXAMPLE
Remove-Comments -SourceFilePath 'C:\Support\GitHub\PSPublishModule\Examples\TestScript.ps1' -DestinationFilePath 'C:\Support\GitHub\PSPublishModule\Examples\TestScript1.ps1' -RemoveAllEmptyLines -RemoveCommentsInParamBlock -RemoveCommentsBeforeParamBlock
.NOTES
Most of the work done by Chris Dent, with improvements by Przemyslaw Klys
#>
[CmdletBinding(DefaultParameterSetName = 'FilePath')]
param(
[Parameter(Mandatory, ParameterSetName = 'FilePath')]
[alias('FilePath', 'Path', 'LiteralPath')][string] $SourceFilePath,
[Parameter(Mandatory, ParameterSetName = 'Content')][string] $Content,
[Parameter(ParameterSetName = 'Content')]
[Parameter(ParameterSetName = 'FilePath')]
[alias('Destination')][string] $DestinationFilePath,
[Parameter(ParameterSetName = 'Content')]
[Parameter(ParameterSetName = 'FilePath')]
[switch] $RemoveAllEmptyLines,
[Parameter(ParameterSetName = 'Content')]
[Parameter(ParameterSetName = 'FilePath')]
[switch] $RemoveEmptyLines,
[Parameter(ParameterSetName = 'Content')]
[Parameter(ParameterSetName = 'FilePath')]
[switch] $RemoveCommentsInParamBlock,
[Parameter(ParameterSetName = 'Content')]
[Parameter(ParameterSetName = 'FilePath')]
[switch] $RemoveCommentsBeforeParamBlock,
[Parameter(ParameterSetName = 'Content')]
[Parameter(ParameterSetName = 'FilePath')]
[switch] $DoNotRemoveSignatureBlock
)
if ($SourceFilePath) {
$Fullpath = Resolve-Path -LiteralPath $SourceFilePath
$Content = [IO.File]::ReadAllText($FullPath, [System.Text.Encoding]::UTF8)
}
$Tokens = $Errors = @()
$Ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$Tokens, [ref]$Errors)
#$functionDefinition = $ast.Find({ $args[0] -is [FunctionDefinitionAst] }, $false)
$groupedTokens = $Tokens | Group-Object { $_.Extent.StartLineNumber }
$DoNotRemove = $false
$DoNotRemoveCommentParam = $false
$CountParams = 0
$ParamFound = $false
$SignatureBlock = $false
$toRemove = foreach ($line in $groupedTokens) {
if ($Ast.Body.ParamBlock.Extent.StartLineNumber -gt $line.Name) {
continue
}
$tokens = $line.Group
for ($i = 0; $i -lt $line.Count; $i++) {
$token = $tokens[$i]
if ($token.Extent.StartOffset -lt $Ast.Body.ParamBlock.Extent.StartOffset) {
continue
}
# Lets find comments between function and param block and not remove them
if ($token.Extent.Text -eq 'function') {
if (-not $RemoveCommentsBeforeParamBlock) {
$DoNotRemove = $true
}
continue
}
if ($token.Extent.Text -eq 'param') {
$ParamFound = $true
$DoNotRemove = $false
}
if ($DoNotRemove) {
continue
}
# lets find comments between param block and end of param block
if ($token.Extent.Text -eq 'param') {
if (-not $RemoveCommentsInParamBlock) {
$DoNotRemoveCommentParam = $true
}
continue
}
if ($ParamFound -and ($token.Extent.Text -eq '(' -or $token.Extent.Text -eq '@(')) {
$CountParams += 1
} elseif ($ParamFound -and $token.Extent.Text -eq ')') {
$CountParams -= 1
}
if ($ParamFound -and $token.Extent.Text -eq ')') {
if ($CountParams -eq 0) {
$DoNotRemoveCommentParam = $false
$ParamFound = $false
}
}
if ($DoNotRemoveCommentParam) {
continue
}
# if token not comment we leave it as is
if ($token.Kind -ne 'Comment') {
continue
}
# kind of useless to not remove signature block if we're not removing comments
# this changes the structure of a file and signature will be invalid
if ($DoNotRemoveSignatureBlock) {
if ($token.Kind -eq 'Comment' -and $token.Text -eq '# SIG # Begin signature block') {
$SignatureBlock = $true
continue
}
if ($SignatureBlock) {
if ($token.Kind -eq 'Comment' -and $token.Text -eq '# SIG # End signature block') {
$SignatureBlock = $false
}
continue
}
}
$token
}
}
$toRemove = $toRemove | Sort-Object { $_.Extent.StartOffset } -Descending
foreach ($token in $toRemove) {
$StartIndex = $token.Extent.StartOffset
$HowManyChars = $token.Extent.EndOffset - $token.Extent.StartOffset
$content = $content.Remove($StartIndex, $HowManyChars)
}
if ($RemoveEmptyLines) {
# Remove empty lines if more than one empty line is found. If it's just one line, leave it as is
#$Content = $Content -replace '(?m)^\s*$', ''
#$Content = $Content -replace "(`r?`n){2,}", "`r`n"
# $Content = $Content -replace "(`r?`n){2,}", "`r`n`r`n"
$Content = $Content -replace '(?m)^\s*$', ''
$Content = $Content -replace "(?:`r?`n|\n|\r)", "`r`n"
}
if ($RemoveAllEmptyLines) {
# Remove all empty lines from the content
$Content = $Content -replace '(?m)^\s*$(\r?\n)?', ''
}
if ($Content) {
$Content = $Content.Trim()
}
if ($DestinationFilePath) {
$Content | Set-Content -Path $DestinationFilePath -Encoding utf8
} else {
$Content
}
}
This function has a couple of parameters:
- SourceFilePath – provide a path to a file you want to clean up
- Content – alternatively to file way, you can also provide a code (for example
Get-Content -Raw $FilePath)
- DestinationFilePath – path to file where to save the cleaned-up file. If not provided, the content will be returned directly as a string.
- RemoveEmptyLines – as part of the cleanup, it tries to remove empty lines, but only if there's more than one. This is useful if you have help with the function
- RemoveAllEmptyLines – removes all empty lines from a file
- RemoveCommentsInParamBlock – by default, during cleanup, any comments inside the param block are not removed, as those are often related to help. But if you want, you can also remove those with this switch.
- RemoveCommentsBeforeParamBlock – I don't remove anything between the function and param block by default. This ensures that the help I create for the function stays where it is. But if you want to remove it, this is how you can fix it.
- DoNotRemoveSignatureBlock – by default, we remove any signature from a file, but if you want to prevent that from happening, you can use this switch. It won't give you much because the signature will not work after you remove anything from the file anyways – but it's there.