Search code examples
powershellpowershell-corescriptblock

Recreate PowerShell Script Block Using Scope


I have the following cmdlet to invoke an arbitrary scriptblock (that usually calls an exe) and handles the return code. The goal here is to print a command line, run it, and then throw an error if it fails....


function Invoke-ScriptBlock {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [scriptblock]$Block
    )

    $ErrorActionPreference = 'Continue'

    $stringBlock = "@`"`n" + $Block.ToString().Trim() + "`n`"@"
    $stringValue = . ([scriptblock]::create($stringBlock))

    Write-Information "Invoking command: $stringValue"

    . $Block
    if ($lastexitcode -ne 0) { Write-Error "Command exited with code $lastexitcode" -EA Stop }
}

$comment = 'acomment'
Invoke-ScriptBlock { git commit -m $comment }
# prints Invoking command: git commit -m acomment

This works great as long as the Invoke-ScriptBlock cmdlet isn't inside a module. If it is, I lose the closure on the variables captured in the "Invoking command" message.

Import-Module .\Util.psm1
$comment = 'acomment'
Invoke-ScriptBlock { git commit -m $comment }
# prints Invoking command: git commit -m

Is there a way I can recreate the scriptblock using the original context from the passed in $Block?


Solution

  • There's no supported way, but you can do it with reflection by copying the session state reference from the original $Block to the recreated script block:

    function Invoke-ScriptBlock {
        [CmdletBinding()]
        param (
            [Parameter(Mandatory)]
            [ValidateNotNullOrEmpty()]
            [scriptblock]$Block
        )
    
        # To prove local and module-scoped variables won't affect the new script block
        $comment = ''
    
        # Obtain a reference to the relevant internal ScriptBlock property 
        $ssip = [scriptblock].GetProperty('SessionStateInternal',[System.Reflection.BindingFlags]'NonPublic,Instance')
    
        # Copy the value from $Block
        $ssi = $ssip.GetMethod.Invoke($Block, @())
    
        # Create new block with the same content 
        $newBlock = [scriptblock]::create("@`"`n" + $Block.ToString().Trim() + "`n`"@")
    
        # Overwrite session state value with the one with copied from $Block
        $ssip.SetMethod.Invoke($newBlock, @($ssi))
    
        $stringValue = & $newBlock
    
        Write-Information "Invoking command: $stringValue"
        & $Block
    }