Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- function Expand-String
- {
- <# .SYNOPSIS Returns code to replace input expansion string as string #>
- [cmdletbinding()]
- Param (
- [string]$String )
- # Get the PowerShell Parser's ScanString method
- # (It's an 'internal' method, so we can't call it with the usual syntax)
- $ScanString = [System.Management.Automation.Language.Parser].GetMethod( 'ScanString', @( 'static','nonpublic','instance' ) )
- # Ask PowerShell to do the work of finding the expressions within the String
- $StringExpression = $ScanString.Invoke( $Null, @( $String ) )
- $Nested = $StringExpression.NestedExpressions.Extent
- # If there are no nested expression
- # (No change needed)
- # Return the original string
- If ( -not $Nested )
- {
- return $String
- }
- # If the first nested expression is the first thing in the string
- # Add it to the expansion, with a cast to [string]
- # (All subsequent elements will be implicitly cast)
- If ( $Nested[0].StartOffset -eq 1 )
- {
- $Expansion = '( [string]' + $Nested[0].Text
- $Position = $Nested[0].EndOffset - 1
- $NestIndex = 1
- }
- # Else (first part of the string is a string)
- # Add it to the expansion
- Else
- {
- $Expansion = '( "' + $String.Substring( 0, $Nested[0].StartOffset - 1 ) + '"'
- $Position = $Nested[0].StartOffset - 1
- $NestIndex = 0
- }
- # While we are not at the end of the string...
- While ( $Position -lt $String.Length )
- {
- # If the next thing in the the string is the next nested expression
- # Add it to the expansion
- If ( $Position -eq $Nested[$NestIndex].StartOffset - 1 )
- {
- $Expansion += " + " + $Nested[$NestIndex].Text
- $Position = $Nested[$NestIndex].EndOffset - 1
- $NestIndex++
- }
- # Else (next thing in the string is a string)...
- Else
- {
- # If there is anothor expression ahead
- # Add a substring of string up to the next expression to the expansion
- If ( $Nested[$NestIndex] )
- {
- $Length = $Nested[$NestIndex].StartOffset - $Position - 1
- $Expansion += ' + "' + $String.Substring( $Position, $Length ) + '"'
- $Position += $Length
- }
- # Else (the rest of the string is nothing but string)
- # Add it to the expansion
- Else
- {
- $Expansion += ' + "' + $String.Substring( $Position ) + '"'
- $Position = $String.Length
- }
- }
- }
- $Expansion += " )"
- # return the result
- return $Expansion
- }
- function Remove-CommentsAndWhiteSpaceAndVariablesAndCommands
- {
- # We are not restricting scriptblock type as Tokenize() can take several types
- Param (
- [parameter( ValueFromPipeline = $True )]
- $Scriptblock
- )
- Begin
- {
- # Intialize collection
- $Items = @()
- }
- Process
- {
- # Collect all of the inputs together
- $Items += $Scriptblock
- }
- End
- {
- ## Process the script as a single unit
- # Convert input to a single string if needed
- $OldScript = $Items -join [environment]::NewLine
- # If no work to do
- # We're done
- If ( -not $OldScript.Trim( " `n`r`t" ) ) { return }
- # Use the PowerShell tokenizer to break the script into identified tokens
- $Tokens = [System.Management.Automation.PSParser]::Tokenize( $OldScript, [ref]$Null )
- #region Expand any expandable strings
- $Expanded = $False
- $ExpandableStringTokens = $Tokens |
- Where-Object { $_.Type -eq 'String' -and
- ( $OldScript.Substring( $_.Start, 1 ) -eq '"' -or
- $OldScript.Substring( $_.Start, 2 ) -eq '@"' ) } |
- Sort-Object -Property Start -Descending
- ForEach ( $Token in $ExpandableStringTokens )
- {
- $String = $OldScript.Substring( $Token.Start, $Token.Length )
- If ( $String.StartsWith( '@' ) )
- {
- $String = $String.Substring( 2, $String.Length - 3 )
- }
- Else
- {
- $String = $String.Substring( 1, $String.Length - 2 )
- }
- $ExpandedString = Expand-String -String $String
- If ( $ExpandedString -ne $String )
- {
- If ( $OldScript.Length -gt $Token.Start + $Token.Length )
- {
- $OldScript =
- $OldScript.Substring( 0, $Token.Start ) +
- $ExpandedString +
- $OldScript.Substring( $Token.Start + $Token.Length )
- }
- Else
- {
- $OldScript =
- $OldScript.Substring( 0, $Token.Start ) +
- $ExpandedString
- }
- $Expanded = $True
- }
- }
- If ( $Expanded )
- {
- # Use the PowerShell tokenizer to break the script into identified tokens
- $Tokens = [System.Management.Automation.PSParser]::Tokenize( $OldScript, [ref]$Null )
- }
- #endregion
- # Define useful, allowed comments
- $AllowedComments = @(
- 'requires'
- '.SYNOPSIS'
- '.DESCRIPTION'
- '.PARAMETER'
- '.EXAMPLE'
- '.INPUTS'
- '.OUTPUTS'
- '.NOTES'
- '.LINK'
- '.COMPONENT'
- '.ROLE'
- '.FUNCTIONALITY'
- '.FORWARDHELPCATEGORY'
- '.REMOTEHELPRUNSPACE'
- '.EXTERNALHELP' )
- # Strip out the Comments, but not useful comments
- # (Bug: This will break comment-based help that uses leading # instead of multiline <#,
- # because only the headings will be left behind.)
- $Tokens = $Tokens.ForEach{
- If ( $_.Type -ne 'Comment' )
- {
- $_
- }
- Else
- {
- $CommentText = $_.Content.Substring( $_.Content.IndexOf( '#' ) + 1 )
- $FirstInnerToken = [System.Management.Automation.PSParser]::Tokenize( $CommentText, [ref]$Null ) |
- Where-Object { $_.Type -ne 'NewLine' } |
- Select-Object -First 1
- If ( $FirstInnerToken.Content -in $AllowedComments )
- {
- $_
- }
- } }
- # Initialize script string
- $NewScriptText = ''
- $SkipNext = $False
- #region Build Variable cache
- # Initialize variable cache
- $VariableMap = @{}
- $VariableNext = 0
- #region Automatic variables
- # Add automatic variables to cache unchanged
- # Add automatic variables to variable cache
- ( Get-Help about_Automatic_Variables ).
- Split( [environment]::Newline ) |
- Where-Object { $_.StartsWith( ' $' ) -or $_.StartsWith( ' $' ) } |
- ForEach-Object { $_.Trim( ' $' ) } |
- ForEach-Object {
- $VariableMap.Add( $_, "`$$_" )
- $VariableMap.Add( "Script:$_", "`$Script:$_" )
- $VariableMap.Add( "Global:$_", "`$Global:$_" )
- $VariableMap.Add( "Local:$_" , "`$Local:$_" ) }
- # endregion
- #region Script parameter variables
- # Add script parameter variables and any other variables referenced in
- # the script param block to the variable cache unchanged
- # Add script parameter variables to variable cache
- $ParameterNames = @()
- # Find the first use of keyword "Param"
- $ParamIndex = 0..($Tokens.Count-1) |
- Where-Object { $Tokens[$_].Content -eq 'param' -and $Tokens[$_].Type -eq 'Keyword' } |
- Select-Object -First 1
- # Find the first use of keyword "Function"
- $FunctionIndex = 0..($Tokens.Count-1) |
- Where-Object { $Tokens[$_].Content -eq 'function' -and $Tokens[$_].Type -eq 'Keyword' } |
- Select-Object -First 1
- # There are script parameters if there is a Param before
- # any Function definitions
- $ScriptParamUsed =
- ( $ParamIndex -lt $FunctionIndex -or
- $FunctionIndex -eq $Null ) -and
- $ParamIndex -ne $Null
- # If script parameters exist...
- If ( $ScriptParamUsed )
- {
- # Start at the opening parenthesis for the Param block
- $ParenIndex = ($ParamIndex+1)..($Tokens.Count-1) |
- Where-Object { $Tokens[$_].Content -eq '(' -and $Tokens[$_].Type -eq 'GroupStart' } |
- Select-Object -First 1
- # If the first parenthesis exists
- # Count parentheses to find the matching closing parenthesis
- If ( $ParenIndex )
- {
- # Starting one parenthesis deep
- $Level = 1
- # Until we climb all the way out or run out scipt...
- # (with embedded increment of token position)
- While ( $Level -gt 0 -and ++$ParenIndex -le $Tokens.Count )
- {
- # If this token is a parenthesis, increment or decrement level
- $Level += ( $Tokens[$ParenIndex].Content.EndsWith( '(' ) -and $Tokens[$ParenIndex].Type -eq 'GroupStart' )
- $Level -= ( $Tokens[$ParenIndex].Content -eq ')' -and $Tokens[$ParenIndex].Type -eq 'GroupEnd' )
- }
- # If we found the closing parenthesis for the param block...
- If ( $Level -eq 0 )
- {
- # Get all of the variables in the param block
- $ParameterNames += ( $ParamIndex..$ParenIndex ).
- Where{ $Tokens[$_].Type -eq 'Variable' }.
- ForEach{ $Tokens[$_].Content }
- # (We'll add them to the cache in along with
- # the Function parameter variables below.)
- # Note the location of the end of the param block
- # (This is a possible location for inserting Set-Alias commands.)
- $LastScriptParamToken = $ParenIndex
- # If the following token is a new line or semicolon
- # Make that the location of the end of the param block
- If ( $Tokens[$ParenIndex+1].Type -in ( 'NewLine', 'StatementSeparator' ) )
- {
- $LastScriptParamToken++
- }
- }
- }
- }
- #endregion
- #region Function parameter variables
- # Add function parameter variables to the variable cache unchanged
- # Find all keyword "Function" tokens
- $FunctionIndices = ( 0..($Tokens.Count-1) ).
- Where{ $Tokens[$_].Content -eq 'function' -and $Tokens[$_].Type -eq 'Keyword' }
- # For each Function token
- :FLoop
- ForEach ( $StartIndex in $FunctionIndices )
- {
- # If the Function token is properly follewed by a function name...
- If ( $Tokens[$StartIndex+1].Type -eq 'CommandArgument' )
- {
- # Get the function name
- $FunctionName = $Tokens[$StartIndex+1].Content
- # Start at the function's open curly brace
- $BraceIndex = ($StartIndex+2)..($Tokens.Count-1) |
- Where-Object { $Tokens[$_].Content -eq '{' -and $Tokens[$_].Type -eq 'GroupStart' } |
- Select-Object -First 1
- # If the open curly brace was found..
- If ( $BraceIndex )
- {
- # Start one level deep
- $Level = 1
- # Until we climb out of the curly braces...
- While ( $Level -gt 0 )
- {
- # If we ran out of script
- # Stop processing Functions
- If ( ++$BraceIndex -ge $Tokens.Count ) { break FLoop }
- # If this token is a curly brace, increment or decrement the level
- $Level += ( $Tokens[$BraceIndex].Content.EndsWith( '{' ) -and $Tokens[$BraceIndex].Type -eq 'GroupStart' )
- $Level -= ( $Tokens[$BraceIndex].Content -eq '}' -and $Tokens[$BraceIndex].Type -eq 'GroupEnd' )
- }
- # Get the function definition
- $FunctionString = $OldScript.Substring( $Tokens[$StartIndex].Start, $Tokens[$BraceIndex].Start - $Tokens[$StartIndex].Start + 1 )
- try
- {
- ## Warning
- ## This loads an untrusted function into memory
- # Run the function definition
- Invoke-Expression $FunctionString
- # Get the function parameters
- # Add them to the list
- $Function = Get-command $FunctionName -CommandType Function
- $ParameterNames += $Function.Parameters.Keys
- }
- catch {}
- }
- }
- }
- # Deduplicate script and function parameter variables
- $ParameterNames = $ParameterNames | Sort-Object -Unique
- # For each script and function parameter
- # Add it to the cache unchanged
- ForEach ( $Name in $ParameterNames )
- {
- If ( $Name -notin $VariableMap.Keys )
- {
- $VariableMap.Add( $Name, "`$$Name" )
- $VariableMap.Add( "Script:$Name", "`$Script:$Name" )
- $VariableMap.Add( "Global:$Name", "`$Global:$Name" )
- $VariableMap.Add( "Local:$Name" , "`$Local:$Name" )
- }
- }
- #endregion
- #endregion
- #region Build command aliases
- # Get all commands used in the script
- # (Includes all cmdlets, functions, aliases, external references, etc.
- $Commands = $Tokens.Where{ $_.Type -eq 'Command' }.Content |
- Sort-Object -Unique
- # Initialize command map
- $CommandMap = @{}
- # Initialize Set-Alias code block
- $AliasCode = [environment]::NewLine
- # Initialize Set-Alias command separator loop
- $AS = 1
- # For each command...
- ForEach ( $i in 0..($Commands.Count - 1) )
- {
- # Add the command and an alias to the map
- $CommandMap.Add( $Commands[$i], "C$i" )
- # Add a Set-Alias command to the code block
- $AliasCode += "sal C$i $($Commands[$i])"
- # Add a separator to the code block
- # (Three commands per line)
- $AliasCode += ( [environment]::NewLine, ';', ';' )[$AS++ % 3 ]
- }
- # Replace the final separator in the code block with a new line
- $AliasCode = $AliasCode.TrimEnd( ';' + [environment]::NewLine ) + [environment]::NewLine
- # Find the last keyword "Using" token
- $LastUsingToken = ($Tokens.Count-1)..0 |
- Where-Object { $Tokens[$_].Content -eq 'Using' -and $Tokens[$_].Type -eq 'KeyWord' } |
- Select-Object -First 1
- # Set if any Using statements are found
- $UsingUsed = [boolean]$LastUsingToken.Count
- # If Using statements exist
- # Find the last token in the last Using statement
- # (This is a possible location for inserting the Set-Alias code block.)
- If ( $UsingUsed )
- {
- While ( $Tokens[++$LastUsingToken].Type -notin ( 'NewLine', 'StatementSeparator' ) -and
- $LastUsingToken -lt $Tokens.Count - 1)
- {}
- }
- # Find the last "#requires" token
- $LastRequiresToken = ($Tokens.Count-1)..0 |
- Where-Object { $Tokens[$_].Type -eq 'Comment' -and $Tokens[$_].Content -like "#Requires*" -and ( $_ -eq 0 -or $Tokens[$_-1].Type -in ( 'NewLine', 'StatementSeparator' ) ) } |
- Select-Object -First 1
- # Set if any #requires statemens are found
- $RequiresUsed = [boolean]$LastRequiresToken.Count
- # If #requires statements exist
- # Find the last token in the last #requires statement
- # (This is a possible location for inserting the Set-Alias code block.)
- If ( $RequiresUsed )
- {
- $LastRequiresToken++
- }
- # Set if the Set-Alias code block can't go at the beginning of the script
- $AliasDeferred = $ScriptParamUsed -or $UsingUsed -or $RequiresUsed
- # Calculate the location after which to insert the Set-Alias code block
- # (After any parameter block, Using statements, or #requires statements
- $AliasToken = [math]::Max( [math]::Max( [int]$LastRequiresToken, [int]$LastScriptParamToken ), [int]$LastUsingToken )
- # If the Set-Alias code block can be inserted at the start of the script
- # Add it to the script
- If ( -not $AliasDeferred )
- {
- $NewScriptText += $AliasCode
- }
- #endregion
- # If there are at least 2 tokens to process...
- If ( $Tokens.Count -gt 1 )
- {
- # For each token (except the last one)...
- ForEach ( $i in ( 0..($Tokens.Count-2) ) )
- {
- # If token is not a line continuation and not a repeated new line or semicolon...
- If ( -not $SkipNext -and
- $Tokens[$i ].Type -ne 'LineContinuation' -and (
- $Tokens[$i ].Type -notin ( 'NewLine', 'StatementSeparator' ) -or
- $Tokens[$i+1].Type -notin ( 'NewLine', 'StatementSeparator', 'GroupEnd' ) ) )
- {
- # Add Token to new script
- switch ( $Tokens[$i].Type )
- {
- 'Variable'
- {
- # If the variable is not in the variable cache
- # Add it to the variable cache
- If ( $Tokens[$i].Content -notin $VariableMap.Keys )
- {
- # Split out the variable name from the scope name
- $OldVariable = $OldScript.Substring( $Tokens[$i].Start, $Tokens[$i].Length ).Trim( '$' )
- $OldVariableSplit = $OldVariable.Split( ':', 2 )
- $VarName, [string]$Scope = $OldVariableSplit[-1..-2]
- # If the scope is a recognized scope...
- If ( $Scope -in @( '', 'Global', 'Local', 'Script' ) )
- {
- # Get the next new variable name
- $NewVariable = 'V' + $VariableNext++
- # Add the variations of the variable to the cache
- $VariableMap.Add( $VarName , "`$$NewVariable" )
- $VariableMap.Add( "Script:$VarName", "`$Script:$NewVariable" )
- $VariableMap.Add( "Global:$VarName", "`$Global:$NewVariable" )
- $VariableMap.Add( "Local:$VarName" , "`$Local:$NewVariable" )
- }
- # Else (unrecognized scope)
- # Add it to the cache unchanged
- Else
- {
- $VariableMap.Add( $OldVariable, "`$$OldVariable" )
- }
- }
- # Add the cached new or unchanged variable reference to new script
- $NewScriptText += $VariableMap[$Tokens[$i].Content]
- }
- 'String'
- {
- # Add the string to the new script
- # (Get it from the old script, as .Content strips out the quote marks)
- $NewScriptText += $OldScript.Substring( $Tokens[$i].Start, $Tokens[$i].Length )
- }
- 'Command'
- {
- # If this command comes after the Set-Alias code block
- If ( $i -ge $AliasToken )
- {
- # Add the appropriate alias to the new script
- $NewScriptText += $CommandMap[$Tokens[$i].Content]
- }
- # Else (this command comes before the Set-Alias commands)
- Else
- {
- # Add the command to the new script unchanged
- $NewScriptText += $Tokens[$i].Content
- }
- }
- Default
- {
- # Add the token to the new script unchanged
- $NewScriptText += $Tokens[$i].Content
- }
- }
- # If the token does not never require a trailing space
- # And the next token does not never require a leading space
- # And this token and the next are on the same line
- # And this token and the next had white space between them in the original...
- If ( $Tokens[$i ].Type -notin ( 'NewLine', 'GroupStart', 'StatementSeparator' ) -and
- $Tokens[$i+1].Type -notin ( 'NewLine', 'GroupEnd', 'StatementSeparator' ) -and
- $Tokens[$i].EndLine -eq $Tokens[$i+1].StartLine -and
- $Tokens[$i+1].StartColumn - $Tokens[$i].EndColumn -gt 0 )
- {
- # Add a space to new script
- $NewScriptText += ' '
- }
- # If the next token is a new line or semicolon following
- # an open parenthesis or curly brace, skip it
- $SkipNext = $Tokens[$i].Type -eq 'GroupStart' -and $Tokens[$i+1].Type -in ( 'NewLine', 'StatementSeparator' )
- }
- # Else (Token is a line continuation or a repeated new line or semicolon)...
- Else
- {
- # [Do not include it in the new script]
- # If the next token is a new line or semicolon following
- # an open parenthesis or curly brace, skip it
- $SkipNext = $SkipNext -and $Tokens[$i+1].Type -in ( 'NewLine', 'StatementSeparator' )
- }
- # If this is the location to insert the Set-Alias command block
- # insert it
- If ( $AliasDeferred -and $i -eq $AliasToken )
- {
- $NewScriptText += $AliasCode
- }
- }
- }
- # If there is a last token to process...
- If ( $Tokens )
- {
- $i++
- # Add last token to new script
- switch ( $Tokens[$i].Type )
- {
- 'Variable'
- {
- # If the variable is not in the variable cache
- # Add it to the variable cache
- If ( $Tokens[$i].Content -notin $VariableMap.Keys )
- {
- # Split out the variable name from the scope name
- $OldVariable = $OldScript.Substring( $Tokens[$i].Start, $Tokens[$i].Length ).Trim( '$' )
- $OldVariableSplit = $OldVariable.Split( ':', 2 )
- $VarName, [string]$Scope = $OldVariableSplit[-1..-2]
- # If the scope is a recognized scope...
- If ( $Scope -in @( '', 'Global', 'Local', 'Script' ) )
- {
- # Get the next new variable name
- $NewVariable = 'V' + $VariableNext++
- # Add the variations of the variable to the cache
- $VariableMap.Add( $VarName , "`$$NewVariable" )
- $VariableMap.Add( "Script:$VarName", "`$Script:$NewVariable" )
- $VariableMap.Add( "Global:$VarName", "`$Global:$NewVariable" )
- $VariableMap.Add( "Local:$VarName" , "`$Local:$NewVariable" )
- }
- # Else (unrecognized scope)
- # Add it to the cache unchanged
- Else
- {
- $VariableMap.Add( $OldVariable, "`$$OldVariable" )
- }
- }
- # Add the cached new or unchanged variable reference to new script
- $NewScriptText += $VariableMap[$Tokens[$i].Content]
- }
- 'String'
- {
- # Add the string to the new script
- # (Get it from the old script, as .Content strips out the quote marks)
- $NewScriptText += $OldScript.Substring( $Tokens[$i].Start, $Tokens[$i].Length )
- }
- 'Command'
- {
- # If this command comes after the Set-Alias code block
- If ( $i -ge $AliasToken )
- {
- # Add the appropriate alias to the new script
- $NewScriptText += $CommandMap[$Tokens[$i].Content]
- }
- # Else (this command comes before the Set-Alias commands)
- Else
- {
- # Add the command to the new script unchanged
- $NewScriptText += $Tokens[$i].Content
- }
- }
- Default
- {
- # Add the token to the new script unchanged
- $NewScriptText += $Tokens[$i].Content
- }
- }
- }
- # Trim any leading new lines from the new script
- $NewScriptText = $NewScriptText.TrimStart( "`n`r;" )
- # Return the new script as the same type as the input
- If ( $Items.Count -eq 1 )
- {
- If ( $Items[0] -is [scriptblock] )
- {
- # Return single scriptblock
- return [scriptblock]::Create( $NewScriptText )
- }
- Else
- {
- # Return single string
- return $NewScriptText
- }
- }
- Else
- {
- # Return array of strings
- return $NewScriptText -Split [environment]::NewLine
- }
- }
- }
Add Comment
Please, Sign In to add comment