FormatPowerShellCode.psm1
## OTHER MODULE FUNCTIONS AND DATA ## #region Private Variables # Current script path [String]$ScriptPath = Split-Path (get-variable myinvocation -scope script).value.Mycommand.Definition -Parent [System.Boolean]$ThisModuleLoaded = $true #endregion Private Variables #region Module Cleanup $ExecutionContext.SessionState.Module.OnRemove = { # cleanup when unloading module (if any) } #endregion Module Cleanup ## PRIVATE MODULE FUNCTIONS AND DATA ## Function Format-ScriptGetKindLines { <# .SYNOPSIS Supplemental function used to get line location of different kinds of AST tokens in a script. .DESCRIPTION Supplemental function used to get line location of different kinds of AST tokens in a script. .PARAMETER Code Multiline or piped lines of code to process. .EXAMPLE PS > $testfile = 'C:\temp\test.ps1' PS > $test = Get-Content $testfile -raw PS > $test | Format-ScriptGetKindLines -Kind "HereString*" | clip Description ----------- Takes C:\temp\test.ps1 as input, formats as the function defines and places the result in the clipboard to be pasted elsewhere for review. .NOTES Author: Zachary Loeber Site: http://www.the-little-things.net/ Requires: Powershell 3.0 Version History 1.0.0 - Initial release #> [CmdletBinding()] param ( [parameter(Position=0, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [String[]]$Code, [parameter(Position=1, HelpMessage='Type of AST kind to retrieve.')] [String]$Kind = "*" ) Begin { $Codeblock = @() $ParseError = $null $Tokens = $null $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." } Process { $Codeblock += $Code } End { $ScriptText = $Codeblock | Out-String Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $TokenKinds = @($Tokens | Where {$_.Kind -like $Kind}) Foreach ($Token in $TokenKinds) { New-Object psobject -Property @{ 'Start' = $Token.Extent.StartLineNumber 'End' = $Token.Extent.EndLineNumber } } Write-Verbose "$($FunctionName): End." } } Function Get-TokenKindLocations { <# .SYNOPSIS Supplemental function used to get exact location of different kinds of AST tokens in a script. .DESCRIPTION Supplemental function used to get exact location of different kinds of AST tokens in a script. .PARAMETER Code Multiline or piped lines of code to process. .EXAMPLE PS > $testfile = 'C:\temp\test.ps1' PS > $test = Get-Content $testfile -raw PS > $test | Get-TokenKindLocations -Kind "HereString*" | clip Description ----------- Takes C:\temp\test.ps1 as input, formats as the function defines and places the result in the clipboard to be pasted elsewhere for review. .NOTES Author: Zachary Loeber Site: http://www.the-little-things.net/ Requires: Powershell 3.0 Version History 1.0.0 - Initial release #> [CmdletBinding()] param ( [parameter(Position=0, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [String[]]$Code, [parameter(Position=1, HelpMessage='Type of AST kind to retrieve.')] [String[]]$Kind = @() ) Begin { $Codeblock = @() $ParseError = $null $Tokens = $null if ($kind.count -gt 0) { $KindMatch = '^(' + (($Kind | %{[Regex]::Escape($_)}) -join '|') + ')$' } else { $KindMatch = '.*' } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." } Process { $Codeblock += $Code } End { $ScriptText = $Codeblock | Out-String Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $TokenKinds = @($Tokens | Where {$_.Kind -match $KindMatch}) Foreach ($Token in $TokenKinds) { New-Object psobject -Property @{ 'Start' = $Token.Extent.StartOffset 'End' = $Token.Extent.EndOffset } } Write-Verbose "$($FunctionName): End." } } # List of token name to string mappings with some unused enums removed # https://msdn.microsoft.com/en-us/library/system.management.automation.language.tokenkind(v=vs.85).aspx $TokenKindDefinitions = @{ 'Ampersand' = '&' 'And' = '-and' 'AndAnd' = '&&' 'As' = '-as' 'AtCurly' = '@{' 'AtParen' = '@(' 'Band' = '-band' 'Begin' = 'Begin' 'Bnot' = '-bnot' 'Bor' = '-bor' 'Break' = 'break' 'Bxor' = '-bxor' 'Catch' = 'catch' 'Ccontains' = '-ccontains' 'Ceq' = '-ceq' 'Cge' = '-cge' 'Cgt' = '-cgt' 'Cin' = '-cin' 'Class' = 'class' 'Cle' = '-cle' 'Clike' = '-clike' 'Clt' = '-clt' 'Cmatch' = '-cmatch' 'Cne' = '-cne' 'Cnotcontains' = '-cnotcontains' 'Cnotin' = '-cnotin' 'Cnotlike' = '-cnotlike' 'Cnotmatch' = '-cnotmatch' 'ColonColon' = '::' 'Comma' = ',' 'Continue' = 'continue' 'Creplace' = '-creplace' 'Csplit' = '-csplit' 'Data' = 'data' 'Define' = 'define' 'Divide' = '/' 'DivideEquals' = '/=' 'Do' = 'do' 'DollarParen' = '$(' 'Dot' = '.' 'DotDot' = '..' 'Dynamicparam' = 'dynamicparam' 'Else' = 'else' 'ElseIf' = 'elseif' 'End' = 'end' 'Enum' = 'enum' 'Equals' = '=' 'Exclaim' = '!' 'Exit' = 'exit' 'Filter' = 'filter' 'Finally' = 'finally' 'For' = 'for' 'Foreach' = 'foreach' 'Format' = '-f' 'From' = 'from' 'Function' = 'function' 'Icontains' = '-contains' 'Ieq' = '-eq' 'If' = 'if' 'Ige' = '-ge' 'Igt' = '-gt' 'Iin' = '-in' 'Ile' = '-le' 'Ilike' = '-like' 'Ilt' = '-lt' 'Imatch' = '-match' 'In' = 'in' 'Ine' = '-ne' 'InlineScript' = 'inlinescript' 'Inotcontains' = '-notcontains' 'Inotin' = '-notin' 'Inotlike' = '-notlike' 'Inotmatch' = '-notmatch' 'Ireplace' = '-replace' 'Is' = '-is' 'IsNot' = '-isnot' 'Isplit' = '-split' 'Join' = '-join' 'LBracket' = '[' 'LCurly' = '{' 'LineContinuation' = '`' 'LParen' = '(' 'Minus' = '-' 'MinusEquals' = '-=' 'MinusMinus' = '--' 'Multiply' = '*' 'MultiplyEquals' = '*=' 'Namespace' = 'namespace' 'NewLine' = '\r\n' 'Not' = '-not' 'Or' = '-or' 'OrOr' = '||' 'Parallel' = 'parallel' 'Param' = 'param' 'Pipe' = '|' 'Plus' = '+' 'PlusEquals' = '+=' 'PlusPlus' = '++' 'PostfixMinusMinus' = '--' 'PostfixPlusPlus' = '++' 'Private' = 'private' 'Process' = 'process' 'Public' = 'public' 'RBracket' = ']' 'RCurly' = '}' 'Rem' = '%' 'RemainderEquals' = '%=' 'Return' = 'return' 'RParen' = ')' 'Semi' = ';' 'Sequence' = 'sequence' 'Shl' = '-shl' 'Shr' = '-shr' 'Static' = 'static' 'Switch' = 'switch' 'Throw' = 'throw' 'Trap' = 'trap' 'Try' = 'try' 'Type' = 'type' 'Until' = 'until' 'While' = 'while' 'Workflow' = 'workflow' 'Xor' = '-xor' } Function Get-BreakableTokens { [CmdletBinding()] param ( [parameter(Position=0, ValueFromPipeline=$true, Mandatory=$true, HelpMessage='Tokens to process.')] [System.Management.Automation.Language.Token[]]$Tokens ) Begin { $Kinds = @('Pipe') # Flags found here: https://msdn.microsoft.com/en-us/library/system.management.automation.language.tokenflags(v=vs.85).aspx $TokenFlags = @('BinaryPrecedenceAdd','BinaryPrecedenceMultiply','BinaryPrecedenceLogical') $Kinds_regex = '^(' + (($Kinds | %{[Regex]::Escape($_)}) -join '|') + ')$' $TokenFlags_regex = '(' + (($TokenFlags | %{[Regex]::Escape($_)}) -join '|') + ')' $Results = @() $AllTokens = @() } Process { $AllTokens += $Tokens } End { Foreach ($Token in $AllTokens) { if (($Token.Kind -match $Kinds_regex) -or ($Token.TokenFlags -match $TokenFlags_regex)) { $Results += $Token } } $Results } } Function Get-CallerPreference { <# .Synopsis Fetches "Preference" variable values from the caller's scope. .DESCRIPTION Script module functions do not automatically inherit their caller's variables, but they can be obtained through the $PSCmdlet variable in Advanced Functions. This function is a helper function for any script module Advanced Function; by passing in the values of $ExecutionContext.SessionState and $PSCmdlet, Get-CallerPreference will set the caller's preference variables locally. .PARAMETER Cmdlet The $PSCmdlet object from a script module Advanced Function. .PARAMETER SessionState The $ExecutionContext.SessionState object from a script module Advanced Function. This is how the Get-CallerPreference function sets variables in its callers' scope, even if that caller is in a different script module. .PARAMETER Name Optional array of parameter names to retrieve from the caller's scope. Default is to retrieve all Preference variables as defined in the about_Preference_Variables help file (as of PowerShell 4.0) This parameter may also specify names of variables that are not in the about_Preference_Variables help file, and the function will retrieve and set those as well. .EXAMPLE Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Imports the default PowerShell preference variables from the caller into the local scope. .EXAMPLE Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState -Name 'ErrorActionPreference','SomeOtherVariable' Imports only the ErrorActionPreference and SomeOtherVariable variables into the local scope. .EXAMPLE 'ErrorActionPreference','SomeOtherVariable' | Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState Same as Example 2, but sends variable names to the Name parameter via pipeline input. .INPUTS String .OUTPUTS None. This function does not produce pipeline output. .LINK about_Preference_Variables #> [CmdletBinding(DefaultParameterSetName = 'AllVariables')] param ( [Parameter(Mandatory = $true)] [ValidateScript({ $_.GetType().FullName -eq 'System.Management.Automation.PSScriptCmdlet' })] $Cmdlet, [Parameter(Mandatory = $true)] [System.Management.Automation.SessionState]$SessionState, [Parameter(ParameterSetName = 'Filtered', ValueFromPipeline = $true)] [String[]]$Name ) Begin { $filterHash = @{} } Process { if ($null -ne $Name) { foreach ($string in $Name) { $filterHash[$string] = $true } } } End { # List of preference variables taken from the about_Preference_Variables help file in PowerShell version 4.0 $vars = @{ 'ErrorView' = $null 'FormatEnumerationLimit' = $null 'LogCommandHealthEvent' = $null 'LogCommandLifecycleEvent' = $null 'LogEngineHealthEvent' = $null 'LogEngineLifecycleEvent' = $null 'LogProviderHealthEvent' = $null 'LogProviderLifecycleEvent' = $null 'MaximumAliasCount' = $null 'MaximumDriveCount' = $null 'MaximumErrorCount' = $null 'MaximumFunctionCount' = $null 'MaximumHistoryCount' = $null 'MaximumVariableCount' = $null 'OFS' = $null 'OutputEncoding' = $null 'ProgressPreference' = $null 'PSDefaultParameterValues' = $null 'PSEmailServer' = $null 'PSModuleAutoLoadingPreference' = $null 'PSSessionApplicationName' = $null 'PSSessionConfigurationName' = $null 'PSSessionOption' = $null 'ErrorActionPreference' = 'ErrorAction' 'DebugPreference' = 'Debug' 'ConfirmPreference' = 'Confirm' 'WhatIfPreference' = 'WhatIf' 'VerbosePreference' = 'Verbose' 'WarningPreference' = 'WarningAction' } foreach ($entry in $vars.GetEnumerator()) { if (([String]::IsNullOrEmpty($entry.Value) -or -not $Cmdlet.MyInvocation.BoundParameters.ContainsKey($entry.Value)) -and ($PSCmdlet.ParameterSetName -eq 'AllVariables' -or $filterHash.ContainsKey($entry.Name))) { $variable = $Cmdlet.SessionState.PSVariable.Get($entry.Key) if ($null -ne $variable) { if ($SessionState -eq $ExecutionContext.SessionState) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } } if ($PSCmdlet.ParameterSetName -eq 'Filtered') { foreach ($varName in $filterHash.Keys) { if (-not $vars.ContainsKey($varName)) { $variable = $Cmdlet.SessionState.PSVariable.Get($varName) if ($null -ne $variable) { if ($SessionState -eq $ExecutionContext.SessionState) { Set-Variable -Scope 1 -Name $variable.Name -Value $variable.Value -Force -Confirm:$false -WhatIf:$false } else { $SessionState.PSVariable.Set($variable.Name, $variable.Value) } } } } } } } Function Get-NewToken { param ( $line ) $results = ( [System.Management.Automation.PSParser]::Tokenize($line, [System.Management.Automation.PSReference]$null) # | # where { # $_.Type -match 'variable|member|command' -and # $_.Content -ne "_" # } ) $results # $(foreach($result in $results) { ConvertTo-CamelCase $result }) -join '' } Function Get-ParentASTTypes { <# .SYNOPSIS Retrieves all parent types of a given AST element. .DESCRIPTION .PARAMETER Code Multiline or piped lines of code to process. .EXAMPLE Description ----------- .NOTES Author: Zachary Loeber Site: http://www.the-little-things.net/ Requires: Powershell 3.0 Version History 1.0.0 - Initial release #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='AST element to process.')] $AST ) # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $ASTParents = @() if ($AST.Parent -ne $null) { $CurrentParent = $AST.Parent $KeepProcessing = $true } else { $KeepProcessing = $false } while ($KeepProcessing) { $ASTParents += $CurrentParent.GetType().Name.ToString() if ($CurrentParent.Parent -ne $null) { $CurrentParent = $CurrentParent.Parent $KeepProcessing = $true } else { $KeepProcessing = $false } } $ASTParents Write-Verbose "$($FunctionName): End." } Function Get-TokenKindLocations { <# .SYNOPSIS Supplemental function used to get exact location of different kinds of AST tokens in a script. .DESCRIPTION Supplemental function used to get exact location of different kinds of AST tokens in a script. .PARAMETER Code Multiline or piped lines of code to process. .EXAMPLE PS > $testfile = 'C:\temp\test.ps1' PS > $test = Get-Content $testfile -raw PS > $test | Get-TokenKindLocations -Kind "HereStringLiteral" | clip Description ----------- Takes C:\temp\test.ps1 as input, formats as the function defines and places the result in the clipboard to be pasted elsewhere for review. .NOTES Author: Zachary Loeber Site: http://www.the-little-things.net/ Requires: Powershell 3.0 Version History 1.0.0 - Initial release #> [CmdletBinding()] param ( [parameter(Position=0, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [String[]]$Code, [parameter(Position=1, HelpMessage='Type of AST kind to retrieve.')] [String[]]$Kind = @() ) Begin { $Codeblock = @() $ParseError = $null $Tokens = $null if ($kind.count -gt 0) { $KindMatch = '^(' + (($Kind | %{[Regex]::Escape($_)}) -join '|') + ')$' } else { $KindMatch = '.*' } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." } Process { $Codeblock += $Code } End { $ScriptText = $Codeblock | Out-String Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $TokenKinds = @($Tokens | Where {$_.Kind -match $KindMatch}) Foreach ($Token in $TokenKinds) { New-Object psobject -Property @{ 'Start' = $Token.Extent.StartOffset 'End' = $Token.Extent.EndOffset } } Write-Verbose "$($FunctionName): End." } } Function Get-TokensBetweenLines { <# .SYNOPSIS Supplemental function used to get all tokens between the lines requested. .DESCRIPTION Supplemental function used to get all tokens between the lines requested. .PARAMETER Code Multiline or piped lines of code to process. .PARAMETER Start Start line to search .PARAMETER End End line to search .EXAMPLE PS > $testfile = 'C:\temp\test.ps1' PS > $test = Get-Content $testfile -raw PS > $test | Get-TokensBetweenLines -Start 47 -End 47 Description ----------- Takes C:\temp\test.ps1 as input, and returns all tokens on line 47. .NOTES Author: Zachary Loeber Site: http://www.the-little-things.net/ Requires: Powershell 3.0 Version History 1.0.0 - Initial release #> [CmdletBinding()] param ( [parameter(ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [String[]]$Code, [parameter(Position=1, ValueFromPipeline=$true, Mandatory=$true, HelpMessage='Type of AST kind to retrieve.')] [System.Int32]$Start, [parameter(Position=2, ValueFromPipeline=$true, Mandatory=$true, HelpMessage='Type of AST kind to retrieve.')] [System.Int32]$End ) Begin { $Codeblock = @() $ParseError = $null $Tokens = $null $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." } Process { $Codeblock += $Code } End { $ScriptText = $Codeblock | Out-String Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $Tokens | Where { ($_.Extent.StartLineNumber -ge $Start) -and ($_.Extent.EndLineNumber -le $End) } Write-Verbose "$($FunctionName): End." } } Function Get-TokensOnLineNumber { [CmdletBinding()] param ( [parameter(Position=0, ValueFromPipeline=$true, Mandatory=$true, HelpMessage='Tokens to process.')] [System.Management.Automation.Language.Token[]]$Tokens, [parameter(Position=1, Mandatory=$true, HelpMessage='Line Number')] [System.Int32]$LineNumber ) Begin { $AllTokens = @() } Process { $AllTokens += $Tokens } End { $AllTokens | Where {($_.Extent.StartLineNumber -eq $_.Extent.EndLineNumber) -and ($_.Extent.StartLineNumber -eq $LineNumber)} } } Function Update-EscapableCharacters { [CmdletBinding()] param ( [parameter(Position=0, ValueFromPipeline=$true, Mandatory=$true, HelpMessage='Line of characters to process.')] [String]$line, [parameter(Position=1, HelpMessage='Type of string to process (single or double quoted)')] [String]$linetype = "'" ) if ($linetype -eq "'") { $retline = $line -replace "'","''" } else { # First normalize any already escaped characters $retline = $line -replace '`"','"' -replace "```'","'" -replace '`#','#' -replace '``','`' # Then re-escape them $retline = $retline -replace '`','``' -replace '"','`"' -replace "'","```'" -replace '#','`#' } if ($retline.length -gt 0) { $linetype + $retline + $linetype + ' + ' + '"`r`n"' } else { if ($retline -match "`r`n") { '"`r`n"' } } } ## PUBLIC MODULE FUNCTIONS AND DATA ## Function Format-ScriptCondenseEnclosures { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Lines of code to look for and condense.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Start of enclosure (typically left parenthesis or curly braces')] [String[]]$EnclosureStart = @('{','(','@{','@('), [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $enclosures = @() $EnclosureStart | foreach {$enclosures += [Regex]::Escape($_)} $regex = '^\s*(' + ($enclosures -join '|') + ')\s*$' $Output = @() $Count = 0 $LineCount = 0 } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") try { $KindLines = @($ScriptText | Format-ScriptGetKindLines -Kind "HereString*") $KindLines += @($ScriptText | Format-ScriptGetKindLines -Kind 'Comment') } catch { throw "$($FunctionName): Unable to properly parse the code for herestrings/comments..." } ($Codeblock -split "`r`n") | Foreach { $LineCount++ if (($_ -match $regex) -and ($Count -gt 0)) { $encfound = $Matches[1] # if the prior line has any kind of comment/hash ignore it if (-not ($Output[$Count - 1] -match '#')) { Write-Verbose "$($FunctionName): Condensed enclosure $($encfound) at line $LineCount" $Output[$Count - 1] = "$($Output[$Count - 1]) $($encfound)" } else { $Output += $_ $Count++ } } else { $Output += $_ $Count++ } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $Output)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $Output Write-Verbose "$($FunctionName): End." } } Function Format-ScriptExpandFunctionBlocks { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Skip expansion of a codeblock if it only has a single line.')] [System.Management.Automation.SwitchParameter]$DontExpandSingleLineBlocks, [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null $predicate = {$args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst]} } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } # First get all blocks $Blocks = $AST.FindAll($predicate, $true) # Just in case we screw up and create more blocks than we started with this will prevent an endless loop $StartingBlocks = $Blocks.count for($t = 0; $t -lt $StartingBlocks; $t++) { Write-Verbose "$($FunctionName): Processing itteration = $($t); Function $($Blocks[$t].Name) ." # We have to reprocess the entire ast lookup process every damn time we make a change. Must be a better way... $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) $Blocks = $AST.FindAll($predicate, $true) $B = $Blocks[$t].Extent.Text $Params = '' if (($Blocks[$t].Parameters).Count -gt 0) { $Params = ' (' + (($Blocks[$t].Parameters).Name.Extent.Text -join ', ') + ')' } $InnerBlock = ($B.Substring($B.indexof('{') + 1,($B.LastIndexOf('}') - ($B.indexof('{') + 1)))).Trim() $codelinecount = @($InnerBlock -split "`r`n").Count $RemoveStart = $Blocks[$t].Extent.StartOffset $RemoveEnd = $Blocks[$t].Extent.EndOffset - $RemoveStart if (($codelinecount -le 1) -and $DontExpandSingleLineBlocks) { $NewExtent = 'Function ' + [String]($Blocks[$t].Name) + $Params + "`r`n{ " + $InnerBlock + " }" } else { $NewExtent = 'Function ' + [String]($Blocks[$t].Name) + $Params + "`r`n{`r`n" + $InnerBlock + "`r`n}" } $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$NewExtent) Write-Verbose "$($FunctionName): Processing function $($Blocks[$t].Name)" } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptExpandNamedBlocks { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Skip expansion of a codeblock if it only has a single line.')] [System.Management.Automation.SwitchParameter]$DontExpandSingleLineBlocks, [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null $predicate = {$args[0] -is [System.Management.Automation.Language.NamedBlockAst] -and (-not $args[0].Unnamed)} } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } # First get all blocks $Blocks = $AST.FindAll($predicate, $true) # Just in case we screw up and create more blocks than we started with this will prevent an endless loop $StartingBlocks = $Blocks.count for($t = 0; $t -lt $StartingBlocks; $t++) { # We have to reprocess the entire ast lookup process every damn time we make a change. Must be a better way... $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) $Blocks = $AST.FindAll($predicate, $true) $B = $Blocks[$t].Extent.Text $InnerBlock = ($B.Substring($B.indexof('{') + 1,($B.LastIndexOf('}') - ($B.indexof('{') + 1)))).Trim() $codelinecount = @($InnerBlock -split "`r`n").Count $RemoveStart = $Blocks[$t].Extent.StartOffset $RemoveEnd = $Blocks[$t].Extent.EndOffset - $RemoveStart if (($codelinecount -le 1) -and $DontExpandSingleLineBlocks) { $NewExtent = [String]($Blocks[$t].Blockkind) + "`r`n{ " + $InnerBlock + " }" } else { $NewExtent = [String]($Blocks[$t].Blockkind) + "`r`n{`r`n" + $InnerBlock + "`r`n}" } $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$NewExtent) Write-Verbose "$($FunctionName): Processing block number $t of blocktype $($Blocks[$t].Blockkind)" } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptExpandParameterBlocks { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Place Parameter typenames on their own line.')] [System.Management.Automation.SwitchParameter]$SplitParameterTypeNames, [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null $predicate = {$args[0] -is [System.Management.Automation.Language.ParamBlockAst]} $typepredicate = {$args[0] -is [System.Management.Automation.Language.TypeConstraintAst]} if ($SplitParameterTypeNames) { $TypeBreak = "`r`n" + ' ' } else { $TypeBreak = "" } } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } # First get all blocks $ParamBlocks = @($AST.FindAll($predicate, $true)) # Just in case we screw up and create more blocks than we started with this will prevent an endless loop $ParamBlockCount = $ParamBlocks.count if ($ParamBlocks.Count -gt 0) { for($t = 0; $t -lt $ParamBlockCount; $t++) { # We have to reprocess the entire ast lookup process every damn time we make a change. Must be a better way... $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $ParamBlocks = $AST.FindAll($predicate, $true) $NewParamBlock = '' $RemoveStart = $null $ParamAttribs = @($ParamBlocks[$t].Attributes | Where {($_.PSobject.Properties.name -match "NamedArguments")}) $AllParams = $ParamBlocks[$t].Parameters # Extrapolate the function the parameter block is from if possible if ([String]::IsNullOrEmpty($($ParamBlocks[$t].Parent.Parent.Name))) { $ParsedFunctionName = 'No function name associated with this parameter block.' } else { $ParsedFunctionName = "Function being parsed = $($ParamBlocks[$t].Parent.Parent.Name)" } Write-Verbose "$($FunctionName): Parsing parameter block. $($ParsedFunctionName)" # Process param block attributes first if they exist if ($ParamAttribs.Count -gt 0) { Write-Verbose "$($FunctionName): Parameter attributes found = $($ParamAttribs.Count)" for($p = 0; $p -lt $ParamAttribs.Count; $p++) { if ($p -eq 0) { $RemoveStart = $ParamAttribs[$p].Extent.StartOffset } $NewParamBlock += $ParamAttribs[$p].Extent.Text + "`r`n" } } # Then process the parameters in the block if ($AllParams.Count -gt 0) { Write-Verbose "$($FunctionName): Parameters in parameter block = $($AllParams.Count)" $NewParamBlock += 'param (' + "`r`n" for ($t2 = 0; $t2 -lt $AllParams.Count; $t2++) { $CurrParam = $AllParams[$t2] $CurrParamType = ($CurrParam.FindAll($typepredicate, $true)).TypeName Write-Verbose "$($FunctionName): Processing Parameter $($CurrParam.Name.Extent.Text)" Write-Verbose "$($FunctionName): ... Parameter Type = $($CurrParamType)" $CurrParam.Attributes | Where {($_.PSobject.Properties.name -match "NamedArguments")} | ForEach { $NewParamBlock += ' ' + $_.Extent.Text + "`r`n" } # switch parameter types don't seem to have an easily grabbable type accelerator shortcut from AST :( if ($CurrParam.Statictype.Name -eq 'SwitchParameter') { $ParamType = 'switch' } else { $ParamType = $CurrParamType } # There is a chance no parameter was defined at all (System.Object is actually the default) # if this is the case then don't put any parameter in the output, Otherwise recreate the parameter line from scratch if (-not [String]::IsNullOrEmpty($ParamType)) { $NewParamBlock += ' ' + '[' + $ParamType + ']' + $TypeBreak + $CurrParam.Name.Extent.Text } else { $NewParamBlock += ' ' + $CurrParam.Name.Extent.Text } if (-not [String]::IsNullOrEmpty($CurrParam.DefaultValue)) { $NewParamBlock += ' = ' + $currparam.DefaultValue.Extent.Text } if ($t2 -lt ($AllParams.Count - 1)) { $NewParamBlock += ',' } else { $NewParamBlock += "`r`n" + ' ' + ')' } $NewParamBlock += "`r`n" } } # If there is no parameter attributes to replace then we are starting at the param block beginning if ($RemoveStart -eq $null) { $RemoveStart = $ParamBlocks[$t].Extent.StartOffset } # replace up to the end of the parameter block $RemoveEnd = $ParamBlocks[$t].Extent.EndOffset - $RemoveStart # do the replacement $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$NewParamBlock) } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText -ShowParsingErrors)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptExpandStatementBlocks { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Skip expansion of a codeblock if it only has a single line.')] [System.Management.Automation.SwitchParameter]$DontExpandSingleLineBlocks, [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null <# Need to find the following: StatementBlockAst not below SubExpressionAST (like $($var)) ScriptBlockExpressionAST (like Foreach {}) NamedBlockAST ScriptBlockAST ParamBlockAST #> $predicate = {$args[0] -is [System.Management.Automation.Language.StatementBlockAst] -and (-not $args[0].Unnamed)} } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } # First get all blocks $Blocks = $AST.FindAll($predicate, $true) # Just in case we screw up and create more blocks than we started with this will prevent an endless loop $StartingBlocks = $Blocks.count for($t = 0; $t -lt $StartingBlocks; $t++) { # We have to reprocess the entire ast lookup process every damn time we make a change. Must be a better way... $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) $Blocks = $AST.FindAll($predicate, $true) $ParentTypes = @(Get-ParentASTTypes $Blocks[$t]) $KeepProcessing = $true if (($ParentTypes -contains 'SubExpressionAST') -or ($Blocks[$t].Extent.GetType().Name -eq 'EmptyScriptExtent') -or ($ParentTypes[0] -eq 'ArrayExpressionAST')) { $KeepProcessing = $false } if ($KeepProcessing) { $B = $Blocks[$t].Extent.Text $InnerBlock = ($B.Substring($B.indexof('{') + 1,($B.LastIndexOf('}') - ($B.indexof('{') + 1)))).Trim() $codelinecount = @($InnerBlock -split "`r`n").Count $RemoveStart = $Blocks[$t].Extent.StartOffset $RemoveEnd = $Blocks[$t].Extent.EndOffset - $RemoveStart if (($codelinecount -le 1) -and $DontExpandSingleLineBlocks) { $NewExtent = [String]($Blocks[$t].Blockkind) + "`r`n{ " + $InnerBlock + " }" } else { $NewExtent = [String]($Blocks[$t].Blockkind) + "`r`n{`r`n" + $InnerBlock + "`r`n}" } $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$NewExtent) Write-Verbose "$($FunctionName): Processing block number $t of blocktype $($Blocks[$t].Blockkind)" } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptExpandTypeAccelerators { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Lines of code to to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Expand all type accelerators to make your code look really complex!')] [System.Management.Automation.SwitchParameter]$AllTypes, [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." # Get all of our accelerator objects $accelerators = [PSObject].Assembly.GetType('System.Management.Automation.TypeAccelerators') # All accelerators returned to a hash $accelhash = $accelerators::get # Now filter all the accelerators we will be expanding. $usedhash = @{} $usedarray = @() $accelhash.Keys | Foreach { if ($AllTypes) { # Get all the accelerator types $usedhash.$_ = $accelhash[$_].FullName $usedarray += $_ } # Get just the non-system accelerators elseif ($accelhash[$_].FullName -notlike "System.*") { $usedhash.$_ = $accelhash[$_].FullName $usedarray += $_ } } $Codeblock = @() $CurrentLevel = 0 $ParseError = $null $Tokens = $null $Indent = (' ' * $Depth) } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): The parser will not work properly with errors in the script, please modify based on the above errors and retry." } for($t = $Tokens.Count - 2; $t -ge 1; $t--) { $Token = $Tokens[$t] $NextToken = $Tokens[$t - 1] if (($token.Kind -match 'identifier') -and ($token.TokenFlags -match 'TypeName')) { if ($usedarray -contains $Token.Text) { $replaceval = $usedhash[$Token.Text] Write-Verbose "$($FunctionName):....Updating to $($replaceval)" $RemoveStart = ($Token.Extent).StartOffset $RemoveEnd = ($Token.Extent).EndOffset - $RemoveStart $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$replaceval) } } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptFormatCodeIndentation { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Lines of code to look for and indent.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Depth for indentation.')] [System.Int32]$Depth = 4, [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose -Message "$($FunctionName): Begin." $Codeblock = @() $CurrentLevel = 0 $ParseError = $null $Tokens = $null $Indent = (' ' * $Depth) } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): The parser will not work properly with errors in the script, please modify based on the above errors and retry." } for($t = $Tokens.Count - 2; $t -ge 1; $t--) { $Token = $Tokens[$t] $NextToken = $Tokens[$t - 1] if ($token.Kind -match '(L|At)Curly') { $CurrentLevel-- } if ($NextToken.Kind -eq 'NewLine' ) { # Grab Placeholders for the Space Between the New Line and the next token. $RemoveStart = $NextToken.Extent.EndOffset $RemoveEnd = $Token.Extent.StartOffset - $RemoveStart $IndentText = $Indent * $CurrentLevel $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$IndentText) } if ($token.Kind -eq 'RCurly') { $CurrentLevel++ } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose -Message "$($FunctionName): End." } } Function Format-ScriptFormatCommandNames { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory = $true, ValueFromPipeline=$true, HelpMessage='Multi-line or piped lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Epand any found aliases.')] [System.Management.Automation.SwitchParameter]$ExpandAliases, [parameter(Position = 2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { if ($script:ThisModuleLoaded -eq $true) { # if we are not using the module then this function likely will not be loaded, if we are then try to inherit the calling script preferences Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): The parser will not work properly with errors in the script, please modify based on the above errors and retry." } $commands = $ast.FindAll({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $true) for($t = $commands.Count - 1; $t -ge 0; $t--) { $command = $commands[$t] if ($command.GetCommandName() -ne $null) { $commandInfo = Get-Command -Name $command.GetCommandName() -ErrorAction SilentlyContinue -Module "*" $commandElement = $command.CommandElements[0] $RemoveStart = ($commandElement.Extent).StartOffset $RemoveEnd = ($commandElement.Extent).EndOffset - $RemoveStart $commandsourceispath = $false if (-not ([String]::IsNullOrWhiteSpace($commandInfo.Source))) { # validate that the command isn't simply an exe or cpl in our path (yeah, we gotta do that) $commandsourceispath = Test-Path $commandInfo.Source } if ($ExpandAliases -and ($commandInfo.CommandType -eq 'Alias')) { if ($command.GetCommandName() -eq '?') { #manually handle "?" because Get-Command and Get-Alias won't. Write-Verbose "$($FunctionName): Detected the Where-Object alias '?'" $ReplacementCommand = 'Where-Object' } else { $ReplacementCommand = $commandInfo.ResolvedCommandName } Write-Verbose "$($FunctionName): Replacing Alias $($command.CommandElements[0].Extent.Text) with $ReplacementCommand." $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$ReplacementCommand) } elseif (($commandInfo -ne $null) -and ($commandInfo.Name -cne $command.GetCommandName()) -and (-not $commandsourceispath)) { # if we have a command, its name isn't case sensitive equal to the get-command version, and the command isn't resolved to a path name # then we can replace it. Write-Verbose "$($FunctionName): Replacing the command $($command.CommandElements[0].Extent.Text) with $($commandInfo.Name)." $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$commandInfo.Name) } } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptFormatTypeNames { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position=1, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } Write-Verbose "$($FunctionName): Attempting to parse TypeExpressions within AST." $predicate = { ($args[0] -is [System.Management.Automation.Language.TypeExpressionAst]) -or ($args[0] -is [System.Management.Automation.Language.TypeConstraintAst]) } $types = $ast.FindAll($predicate, $true) for($t = $types.Count - 1; $t -ge 0; $t--) { $type = $types[$t] $typeName = $type.TypeName.Name $extent = $type.TypeName.Extent $FullTypeName = Invoke-Expression "$type" if ($typeName -eq $FullTypeName.Name) { $NameCompare = ($typeName -cne $FullTypeName.Name) $Replacement = $FullTypeName.Name } else { $NameCompare = ($typeName -cne $FullTypeName.FullName) $Replacement = $FullTypeName.FullName } if (($FullTypeName -ne $null) -and ($NameCompare)) { $RemoveStart = $extent.StartOffset $RemoveEnd = $extent.EndOffset - $RemoveStart $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$Replacement) Write-Verbose "$($FunctionName): Replaced $($typeName) with $($Replacement)." } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptPadExpressions { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position=1, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null $predicate = { ($args[0] -is [System.Management.Automation.Language.CommandExpressionAst]) -and (($args[0].FindAll($predicate2,$true)).count -gt 0) } $predicate2 = {$args[0] -is [System.Management.Automation.Language.BinaryExpressionAst]} } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $expressions = $ast.FindAll($predicate, $true) for($t = $expressions.Count - 1; $t -ge 0; $t--) { $expression = $expressions[$t] $tmpexpression = $expression $EmbeddedCommandExpressionAST = $false # Recurse through the parent nodes and look for embedded commandexpressionast types and skip them if found, # (There must be a better way to do this....) while ($tmpexpression.Parent -ne $null) { if ($tmpexpression.Parent.GetType().Name -eq 'CommandExpressionAST') { $EmbeddedCommandExpressionAST = $true Write-Verbose "$($FunctionName): Expression is part of a larger command expression, skipping: $($expression.expression)" } $tmpexpression = $tmpexpression.Parent } if (-not $EmbeddedCommandExpressionAST) { $RemoveStart = $expression.Extent.StartOffset $RemoveEnd = $expression.Extent.EndOffset - $RemoveStart $ExpressionString = $expression.Extent.Text $AST2 = [System.Management.Automation.Language.Parser]::ParseInput($ExpressionString, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) $binaryexpressions = $AST2.FindAll($predicate2,$true) $binaryexpressioncount = $binaryexpressions.count for($t2 = 0; $t2 -lt $binaryexpressioncount; $t2++) { $AST2 = [System.Management.Automation.Language.Parser]::ParseInput($ExpressionString, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) $binaryexpressions = $AST2.FindAll($predicate2,$true) $exp = $binaryexpressions[$t2] $expbegin = $exp.extent.StartOffset $expend = $exp.Extent.EndOffset - $expbegin $expreplace = $exp.Left.Extent.Text + ' ' + $exp.ErrorPosition.Text + ' ' + $exp.Right.Extent.Text $ExpressionString = $ExpressionString.Remove($expbegin,$expend).Insert($expbegin,$expreplace) } Write-Verbose "$($FunctionName): Binary Expressions found in $($expression.expression)" $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$ExpressionString) } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptPadOperators { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position=1, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null $operatorlist = @('Equals','Minus','Plus','MinusEquals','PlusEquals','Divide','DivideEquals','Multiply','MultiplyEquals','Rem','RemainderEquals') $predicate = { ($args[0] -is [System.Management.Automation.Language.AssignmentStatementAst]) -and ($operatorlist -contains $args[0].Operator) -and ($args[0].Left -is [System.Management.Automation.Language.VariableExpressionAst]) } } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $assignments = $ast.FindAll($predicate, $true) for($t = $assignments.Count - 1; $t -ge 0; $t--) { $assignment = $assignments[$t] [String]$NewExtent = '' # This causes extra processing but accounts for embedded assignments like $a=$b=$c=0 $subassignments = ($assignments[$t]).FindAll($predicate, $true) for($t2 = 0; $t2 -lt $subassignments.Count; $t2++) { $NewExtent += $subassignments[$t2].Left.Extent.Text + ' ' + $subassignments[$t2].ErrorPosition.Text + ' ' if ($t2 -eq ($subassignments.Count - 1)) { $NewExtent += $subassignments[$t2].Right.Extent.Text } $RemoveStart = $assignment.Extent.StartOffset $RemoveEnd = $assignment.Extent.EndOffset - $RemoveStart $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$NewExtent) } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptReduceLineLength { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position=1, HelpMessage='Number of characters to shorten long lines to. Default is 115 characters.')] [System.Int32]$Length = 115, [parameter(Position=2, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null } Process { $Codeblock += $Code } End { # Note: I purposefully leave the extra carriage return on $ScriptText to get around an issue with a single line script being passed $ScriptText = $Codeblock | Out-String Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } #$LongLines = @() #$LongLinecount = 0 $SplitScriptText = @($ScriptText -split "`r`n") $OutputScript = @() for($t = 0; $t -lt $SplitScriptText.Count; $t++) { [String]$CurrentLine = $SplitScriptText[$t] Write-Debug "Line - $($t): $CurrentLine" if ($CurrentLine.Length -gt $Length) { $CurrentLineLength = $CurrentLine.Length # find spaces at the beginning of our line. if ($CurrentLine -match '^([\s]*).*$') { $Padding = $Matches[1] $PaddingLength = $Matches[1].length } else { $Padding = '' $PaddingLength = 0 } $AdjustedLineLength = $Length - $PaddingLength $BreakableTokens = @() if ($Tokens -ne $null) { $AllTokensOnLine = $Tokens | Get-TokensOnLineNumber -LineNumber ($t + 1) $BreakableTokens = @($AllTokensOnLine | Get-BreakableTokens) } $DesiredBreakPoints = [Math]::Round($SplitScriptText[$t].Length / $AdjustedLineLength) if ($BreakableTokens.Count -gt 0) { Write-Debug "$($FunctionName): Total String Length: $($CurrentLineLength)" Write-Debug "$($FunctionName): Breakpoint Locations: $($BreakableTokens.Extent.EndColumnNumber -join ', ')" Write-Debug "$($FunctionName): Padding: $($PaddingLength)" Write-Debug "$($FunctionName): Desired Breakpoints: $($DesiredBreakPoints)" if (($BreakableTokens.Count -eq 1) -or ($DesiredBreakPoints -ge $BreakableTokens.Count)) { # if we only have a single breakpoint or the total breakpoints available is equal or less than our desired breakpoints # then simply split the line at every breakpoint. $TempBreakableTokens = @() $TempBreakableTokens += 0 $TempBreakableTokens += $BreakableTokens | Foreach { $_.Extent.EndColumnNumber - 1 } $TempBreakableTokens += $CurrentLine.Length for($t2 = 0; $t2 -lt $TempBreakableTokens.Count - 1; $t2++) { $OutputScript += $Padding + ($CurrentLine.substring($TempBreakableTokens[$t2],($TempBreakableTokens[$t2 + 1] - $TempBreakableTokens[$t2]))).Trim() } } else { # Otherwise we need to selectively break the lines down $TempBreakableTokens = @(0) # Start at the beginning always # We need to adjust our segment length to account for padding we will be including into each segment # to keep the resulting output aligned at the same column it started in. $TotalAdjustedLength = $CurrentLineLength + ($DesiredBreakPoints * $PaddingLength) $SegmentMedianLength = [Math]::Round($TotalAdjustedLength / ($DesiredBreakPoints + 1)) $TokenStartOffset = 0 # starting at the beginning of the string for($t2 = 0; $t2 -lt $BreakableTokens.Count; $t2++) { $TokenStart = $BreakableTokens[$t2].Extent.EndColumnNumber $NextTokenStart = $BreakableTokens[$t2 + 1].Extent.EndColumnNumber if ($t2 -eq 0) { $TokenSize = $TokenStart } else { $TokenSize = $TokenStart - $BreakableTokens[$t2 - 1].Extent.EndColumnNumber } $NextTokenSize = $NextTokenStart - $TokenStart if ((($TokenStartOffset + $TokenSize) -ge $SegmentMedianLength) -or ($NextTokenSize -ge ($SegmentMedianLength - $TokenSize)) -or (($TokenStartOffset + $TokenSize + $NextTokenSize) -gt $SegmentMedianLength)) { $TempBreakableTokens += $BreakableTokens[$t2].Extent.EndColumnNumber - 1 $TokenStartOffset = 0 } else { $TokenStartOffset = $TokenStartOffset + $TokenSize } } $TempBreakableTokens += $CurrentLine.Length for($t2 = 0; $t2 -lt $TempBreakableTokens.Count - 1; $t2++) { Write-Verbose "$($FunctionName): Inserting break in line $($t) at column $($TempBreakableTokens[$t2])" $OutputScript += $Padding + ($CurrentLine.substring($TempBreakableTokens[$t2],($TempBreakableTokens[$t2 + 1] - $TempBreakableTokens[$t2]))).Trim() } } } else { # This line is long and has no plausible breaking points, oh well. $OutputScript += $CurrentLine } } else { # This line doesn't need to be shortened. $OutputScript += $CurrentLine } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $OutputScript Write-Verbose "$($FunctionName): End." } } Function Format-ScriptRemoveStatementSeparators { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null $looppredicate = { ($args[0] -is [System.Management.Automation.Language.LoopStatementAst]) } $loopendpredicate = { ($args[0] -is [System.Management.Automation.Language.StatementBlockAst]) } $hashpredicate = { ($args[0] -is [System.Management.Automation.Language.HashtableAst]) } } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $forloopblocks = @() $loopstatements = $ast.FindAll($looppredicate, $true) $hashstatements = $ast.FindAll($hashpredicate, $true) $semicolontokens = $Tokens | Where {$_.Kind -eq 'Semi'} # get the begin and end positions of every for loop foreach ($loop in $loopstatements) { $forloopblocks += New-Object -TypeName PSObject -Property @{ 'loopstart' = $loop.Extent.StartOffSet 'loopend' = ($loop.FindAll($loopendpredicate, $true))[0].Extent.StartOffSet } } for($t = $semicolontokens.Count - 1; $t -ge 0; $t--) { $semi = $semicolontokens[$t] $ProcessSemi = $true foreach ($loopblock in $forloopblocks) { if (($semi.Extent.StartOffset -le $loopblock.loopend) -and ($semi.Extent.EndOffset -ge $loopblock.loopstart)) { $ProcessSemi = $false } } if ($ProcessSemi) { $RemoveStart = $semi.Extent.StartOffset $RemoveEnd = $semi.Extent.EndOffset - $RemoveStart $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,"`r`n") } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptRemoveSuperfluousSpaces { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ScriptText = @() } Process { $Codeblock += ($Code -split "`r`n") } End { try { $KindLines = @($Codeblock | Format-ScriptGetKindLines -Kind "HereString*") $KindLines += @($Codeblock | Format-ScriptGetKindLines -Kind 'Comment') } catch { throw 'Unable to properly parse the code for herestrings...' } $currline = 0 foreach ($codeline in ($Codeblock -split "`r`n")) { $currline++ $isherestringline = $false $KindLines | Foreach { if (($currline -ge $_.Start) -and ($currline -le $_.End)) { $isherestringline = $true } } if ($isherestringline -eq $true) { $ScriptText += $codeline } else { $ScriptText += $codeline.TrimEnd() } } $ScriptText = ($ScriptText | Out-String).Trim("`r`n") # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptReplaceHereStrings { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } for($t = $Tokens.Count - 2; $t -ge 2; $t--) { $token = $tokens[$t] if ($token.Kind -like "HereString*") { switch ($token.Kind) { 'HereStringExpandable' { $NewStringOp = '"' } default { $NewStringOp = "'" } } $HereStringVar = $tokens[$t - 2].Text $HereStringAssignment = $tokens[$t - 1].Text $RemoveStart = $tokens[$t - 2].Extent.StartOffset $RemoveEnd = $Token.Extent.EndOffset - $RemoveStart $HereStringText = @($Token.Value -split "`r`n") $NewJoinString = @() for ($t2 = 0; $t2 -lt ($HereStringText.Count); $t2++) { $NewJoinString += Update-EscapableCharacters $HereStringText[$t2] $NewStringOp } $CodeReplacement = $HereStringVar + ' ' + $HereStringAssignment + ' ' + (($NewJoinString | Where {-not [String]::IsNullOrEmpty($_)}) -join ' + ') $ScriptText = $ScriptText.Remove($RemoveStart,$RemoveEnd).Insert($RemoveStart,$CodeReplacement) } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText -ShowParsingErrors )) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): End." } } Function Format-ScriptReplaceInvalidCharacters { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position = 0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position = 1, HelpMessage='Bypass code validity check after modifications have been made.')] [System.Management.Automation.SwitchParameter]$SkipPostProcessingValidityCheck ) Begin { # Pull in all the caller verbose,debug,info,warn and other preferences if ($script:ThisModuleLoaded -eq $true) { Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState } $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null $Replacements = 0 } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") # Grab a bunch of start and end character locations for different token types for later filtering. $stinglocations = @($ScriptText | Get-TokenKindLocations -kind 'StringLiteral','StringExpandable') $herestinglocations = @($ScriptText | Get-TokenKindLocations -kind 'HereStringLiteral','HereStringExpandable') $commentlocations = @($ScriptText | Get-TokenKindLocations -kind 'Comment') Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { $ParseError | Write-Error throw "$($FunctionName): Will not work properly with errors in the script, please modify based on the above errors and retry." } $InvalidChars = [Regex]::Matches($ScriptText,('“|”' + "|‘|’")) Foreach ($InvChar in $InvalidChars) { $ShouldReplace = $true # Ensure the invalid character isn't embedded in a comment $commentlocations | Foreach { if (($InvChar.Index -gt $_.Start) -and ($InvChar.Index -lt ($_.End - 1))) { Write-Verbose "$($FunctionName): Not replacing $($InvChar.Value) at $($InvChar.Index) as it was found in a comment." $ShouldReplace = $false } } if ($ShouldReplace) { # ..or a string $stinglocations | Foreach { if (($InvChar.Index -gt $_.Start) -and ($InvChar.Index -lt ($_.End - 1))) { Write-Verbose "$($FunctionName): Not replacing $($InvChar.Value) at $($InvChar.Index) as it was found in a string." $ShouldReplace = $false } } } if ($ShouldReplace) { # ..or a herestring $herestinglocations | Foreach { if (($InvChar.Index -gt ($_.Start + 1)) -and ($InvChar.Index -lt ($_.End - 2))) { Write-Verbose "$($FunctionName): Not replacing $($InvChar.Value) at $($InvChar.Index) as it was found in a herestring" $ShouldReplace = $false } } } if ($ShouldReplace) { switch -regex ($InvChar.Value) { "\‘|’" { Write-Verbose "$($FunctionName): Replacing $($InvChar.Value) with single quote at $($InvChar.Index)." $ScriptText = $ScriptText.Remove($InvChar.Index ,1).Insert($InvChar.Index,"'") $Replacements++ } '“|”' { Write-Verbose "$($FunctionName): Replacing $($InvChar.Value) with double quote at $($InvChar.Index)." $ScriptText = $ScriptText.Remove($InvChar.Index ,1).Insert($InvChar.Index,'"') $Replacements++ } } } } # Validate our returned code doesn't have any unintentionally introduced parsing errors. if (-not $SkipPostProcessingValidityCheck) { if (-not (Format-ScriptTestCodeBlock -Code $ScriptText)) { throw "$($FunctionName): Modifications made to the scriptblock resulted in code with parsing errors!" } } $ScriptText Write-Verbose "$($FunctionName): Total invalid characters replaced = $Replacements" Write-Verbose "$($FunctionName): End." } } Function Format-ScriptTestCodeBlock { <# .EXTERNALHELP FormatPowershellCode-help.xml #> [CmdletBinding()] param ( [parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true, HelpMessage='Lines of code to process.')] [AllowEmptyString()] [String[]]$Code, [parameter(Position=1, HelpMessage='Display parsing errors.')] [System.Management.Automation.SwitchParameter]$ShowParsingErrors ) Begin { $FunctionName = $MyInvocation.MyCommand.Name Write-Verbose "$($FunctionName): Begin." $Codeblock = @() $ParseError = $null $Tokens = $null } Process { $Codeblock += $Code } End { $ScriptText = ($Codeblock | Out-String).trim("`r`n") Write-Verbose "$($FunctionName): Attempting to parse AST." $AST = [System.Management.Automation.Language.Parser]::ParseInput($ScriptText, [System.Management.Automation.PSReference]$Tokens, [System.Management.Automation.PSReference]$ParseError) if($ParseError) { if ($ShowParsingErrors) { $ParseError | Write-Error } return $false } else { return $true } Write-Verbose "$($FunctionName): End." } } |