Renaming all variables in a scriptblock

This topic contains 3 replies, has 2 voices, and was last updated by Profile photo of Adam Bertram Adam Bertram 1 year, 9 months ago.

  • Author
    Posts
  • #26520
    Profile photo of Adam Bertram
    Adam Bertram
    Moderator

    Let's say I have two variables and a scriptblock that looks like this:

    $var1 = 'test1'
    $var2 = 'test2'
    $ScriptBlock = { echo "$var1"; $var2 }
    

    I want to rename $var1 and $var2 to $var1-new and $var2 to $var2-new. What's the best way to do this? I want to be able to rename all variables in a scriptblock regardless of where they are. This is just a simple example. A scriptblock may actually be hundreds of lines long with dozens of variables all in different contexts so I'd appreciate a scalable solution.

    I don't want to just do a regex find/replace on all "$something" strings. I want put some intelligence behind it and pick out the variables in various contexts like by themselves and in double quotes for example.

    I've been fooling around with the AST do this but I don't understand where it's getting it's offset numbers from. Here's what I have.

    $AstVariables = $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)
    $BlockText = $ScriptBlock.Ast.Endblock.Extent.Text
    $selectProps = @(
        @{ n = 'Variable'; e = { $_.VariablePath.UserPath } }
        @{ n = 'Start'; e = { $_.Extent.StartOffset - 16 } }
        @{ n = 'End'; e = { $_.Extent.EndOffset - 16 } }
        'Parent'
    )
    $VariableLocations = $AstVariables | Select-Object -Property $selectProps | Sort-Object 'Start' -Descending
    $VariableLocations | foreach {
        if ($_.Parent.CommandElements[0].StringConstantType -ne 'DoubleQuoted')
        {
            $NewName = '{0}:{1}' -f '$using', $_.Variable
        } else {
            $NewName = '$({0}:{1})' -f '$using', $_.Variable
        }
        $BlockText = $BlockText.Remove($_.Start, ($_.End - $_.Start)).Insert($_.Start, $NewName)
    }
    [scriptblock]::Create($BlockText) 

    This works...sometimes. For some reason, the StartOffset and EndOffset numbers are sometimes wrong to locate the starting and ending positions of the variables. I'm not quite sure why I'm having to subtract 16 from the number to make them close. Admittedly, this is a major hack. There's got to be a better way to get the starting and ending places where all the variables are located in the scriptblock. That is the hardest part so far.

    I've tried to use the Parser as well but ran into some problems when variables were inside double quotes.

    Can anyone offer any assistance?

  • #26525
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    IseSteroids does this. Put the cursor on the variable and press F2 to highlight all instances of it, type the new name, and press Enter to save the change. (When the variable you're changing is a parameter, it will also modify calls to the function, which is pretty slick.)

    I know it's using the AST, but I don't know exactly what the code looks like.

  • #26526
    Profile photo of Dave Wyatt
    Dave Wyatt
    Moderator

    BTW, I can tell you that the offset problems you were seeing are due to how the script block's Ast property is populated. When it's a literal script block in a script file, the offsets are all relative to the start of the file, not the start of the script block. That information is what enables the debugger to trigger based on file and line number, etc.

    Testing:

    'This is a single-quoted string.'
    

    'This is a single-quoted string.'

  • #26528
    Profile photo of Adam Bertram
    Adam Bertram
    Moderator

    Thanks, Dave,

    I managed to calculate the position based on the position of the scriptblock itself. Here's the final code. It seems to work but time will tell once I run a few different scriptblocks through it.

    $ScriptBlock = { echo "$var1"; $var2 }
    $AstVariables = $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.VariableExpressionAst] }, $true)
    $BlockText = $ScriptBlock.Ast.Endblock.Extent.Text
    				$selectProps = @(
    				@{ n = 'Variable'; e = { $_.VariablePath.UserPath } }
    				@{ n = 'Start'; e = { $_.Extent.StartOffset - $ScriptBlock.Ast.Extent.StartOffset -2 } }
    				@{ n = 'End'; e = { $_.Extent.EndOffset - $ScriptBlock.Ast.Extent.StartOffset - 2} }
    				'Parent'
    				)
    				$VariableLocations = $AstVariables | Select-Object -Property $selectProps | Sort-Object 'Start' -Descending
    				$VariableLocations | foreach {
    					## If the variable is not inside double quotes and won't need to be expanded
    					if ($_.Parent.Extent.Text -notmatch '^".*"$')
    					{
    						$NewName = '{0}:{1}' -f '$using', $_.Variable
    					}
    					else
    					{
    						$NewName = '$({0}:{1})' -f '$using', $_.Variable
    					}
    					$StartIndex = $_.Start
    					$EndIndex = $_.End - $_.Start
    					$BlockText = $BlockText.Remove($StartIndex, $EndIndex).Insert($StartIndex, $NewName)
    				}
    				[scriptblock]::Create($BlockText)
    

You must be logged in to reply to this topic.