Variables losing value in a scriptblock when passed to a function

This topic contains 10 replies, has 2 voices, and was last updated by  Max Kozlov 2 years, 5 months ago.

  • Author
    Posts
  • #26785

    Adam Bertram
    Moderator
    function func1 {
    $variable1 = 'somevariable'
    $ScriptBlock = { echo $variable1 }
    func2 -ScriptBlock $ScriptBlock
    }
    
    function func2 {
    param($ScriptBlock)
    ## Open up $ScriptBlock and replace the text $variable1 with $using:variable1 here
    Invoke-Command -Computer somecomputer -ScriptBlock $ScriptBlock
    }

    I would expect this to output 'somevariable' but instead I receive an error: "The value of the using variable '$using:variable1' cannot be retrieved because it has not been set in the local session."

    When the scriptblock is passed from one function to another the value associated with $variable1 is stripped. Is there a way to pass that scriptblock from one function to the other and still be able to get it to work?

  • #26805

    Max Kozlov
    Participant

    Adam, in your example you try to use locally defined variable in remote session. It is a cause of $null i think

    You can send variable value to remote thru -argumentlist or use splatting like
    here https://mjolinor.wordpress.com/2014/01/24/splatting-parameters-pt-2-remote-possibilities/

    btw, this variant show the variable value

    function func1 {
    $variable1 = 'somevariable'
    $ScriptBlock = { echo $using:variable1 }
    func2 -ScriptBlock $ScriptBlock
    }
    
    function func2 {
    param($ScriptBlock)
    Invoke-Command -computer somecomputer -ScriptBlock $ScriptBlock
    }
  • #26816

    Adam Bertram
    Moderator

    Thanks for the reply, Max. I realize from that code that I gave it looks like I'm just passing the local variable and using the argument list is a method to do this but the script I'm having a problem with is much more complicated than that. I probably should have went into a little more detail on what I'm trying to do.

    I'm using the AST to parse out and rename variables inside the scriptblock. I have an Invoke-ScriptBlock function that allows the user to pass a scriptblock with local session variables inside and a computername. If the computer name is local it will simply execute the scriptblock. If it's remote, it will find all [VariableExpressionAst] objects in the AST tree and attempt to prepend "$using:" to them as to make those variables expand prior to getting serialized over a WinRM session.

    I had a small comment about "Open up $Scriptblock" in my example where I was trying to explain this.

  • #26821

    Max Kozlov
    Participant

    [time passed... :)]

    I compile all your code and it works for me if it in one scope (one module, one session)
    But if I get func2 from module, but func1 from cmdline, I get your error

    and THERE is the problem. different contexts 🙂
    and because you construct new scriptblock you get it without original variable context
    but unfortunately I cant find any way to get this context of a script block. Just description of it in .GetNewClosure() method

    simple test

    $m = new-module {
    function func2 {
    param($ScriptBlock)
    #dir variable:
    'vf2';
    get-variable variable1;
    
    & $ScriptBlock
    
    }
    export-modulemember -function func2
    }
    
    function func1 {
    $variable1 = 'somevariable'
    $ScriptBlock = { 'vf1'; get-variable variable1; echo "var1: $variable1" } #.GetNewClosure()
    func2 -ScriptBlock $ScriptBlock
    }
    
    func1


    vf2
    get-variable : Cannot find a variable with the name 'variable1'.
    At line:10 char:1
    + get-variable variable1;
    + ~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : ObjectNotFound: (variable1:String) [Get-Variable], ItemNotFoundException
    + FullyQualifiedErrorId : VariableNotFound,Microsoft.PowerShell.Commands.GetVariableCommand

    vf1

    Name Value
    ---- -----
    variable1 somevariable
    var1: somevariable

  • #26849

    Max Kozlov
    Participant

    hmm, this is some hidden scope, because
    if my scriptblock is get-variable variable1
    'Global','Local','Script' | %{ get-variable variable1 -Scope $_}

    I get variable1 on first try but not any other 3 tries

  • #26850

    Max Kozlov
    Participant

    i find it in parent scope
    get-variable variable1 -Scope 1`
    but only inside func1

  • #26858

    Max Kozlov
    Participant

    I Get it to work (pretty ugly variant, need some work, but not today 🙂 )

    function func1 {
    $variable1 = 'somevariable'
    $ScriptBlock = { echo $variable1 }
    func2 -ScriptBlock $ScriptBlock.GetNewClosure()
    }
    
    function func2 {
    param($ScriptBlock)
    ## Open up $ScriptBlock and replace the text $variable1 with $using:variable1 here
    
    $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)
    }
    # here is the magic
    $SB = $ScriptBlock.Module.NewBoundScriptBlock([scriptblock]::Create($BlockText))
    $SB1 = $ScriptBlock.Module.NewBoundScriptBlock({param($name) Get-Variable $name})
    
    $AstVariables | Foreach-Object{
    #TODO: here we need filtering, we do not want to import the same variables all the time
    	$varname = $_.VariablePath.UserPath
    	New-Variable -Name $varname -Value (& $SB1 $varname)
    }
    
    Invoke-Command -Computer somecomputer -ScriptBlock $SB
    }
    
    export-modulemember -function func2
    

    pay attention that func1 call func2 with GetNewClosure() – Is's important

  • #26859

    Adam Bertram
    Moderator

    Thanks for your hard work on this, Max! I'm planning on giving it a shot today.

  • #26885

    Adam Bertram
    Moderator

    Thanks, Max. That looks like that did it!

  • #26907

    Max Kozlov
    Participant

    And where is the final variant ? 🙂
    btw, I make an error here. we need only value
    $SB1 = $ScriptBlock.Module.NewBoundScriptBlock({param($name) Get-Variable $name -ValueOnly})

    Besides variable filtering there is potential to optimize it removing (get-variable) but only if there is any way to use $variable:variablename syntax or any other way to get variable value by its name but do not use cmdlet.

    And finally, in a function func2 we need to use highly unique internal variable names so we do not overwrite it with $Scriptblock variables when importing it in our own context

  • #26910

    Max Kozlov
    Participant

    find a better way for variable setting

    function func2 {
    [CmdletBinding()]
    ....
    $AstVariables | Foreach-Object{
    #TODO: here we need filtering, we do not want to import the same variables all the time
    $varname = $_.VariablePath.UserPath
    New-Variable -Name $varname -Value ($PSCmdlet.SessionState.PSVariable.GetValue($varname))
    }

    $SB1 do not needed
    thanks to http://get-powershell.com/ (link from your twitter 🙂 )

You must be logged in to reply to this topic.