1

Suppose you have defined two functions in a module (i.e. a .psm1 file):

function f1{
    param($x1)
    $a1 = 10
    f2 $x1
}

function f2{
    param($x2)
    $a2 = 100
    & $x2
}

Now suppose you run the following:

PS C:\> $a0 = 1
PS C:\> $x0 = {$a0+$a1+$a2}
PS C:\> f1 $x0
1

$x2 keeps the context of the command line despite being invoked inside $f2. This holds if you change & to ..

Replacing $xn with $xn.GetNewClosure() in the module then calling f1 captures the value of 100 but not 10:

PS C:\> f1 $x0
101
PS C:\> f1 $x0.GetNewClosure
101

This happens because calling .GetNewClosure() inside f2 "overwrites" the value of $a1 captured in f1.

Is there a way to selectively capture variables in scriptblocks? Working from the example, is there a way to capture both $a1 inside f1 and $a2 inside f2?


Further Reading

PowerShell scopes are not simple. Consider the possibilities from this incomplete list of factors:

  • there can be any combination of global and module scope hierarchies active at any time
  • . and & invocation affects scope differently,
  • the sophisticated flow control afforded by the pipeline means that multiple scopes of the begin, process, and end scriptblocks of different or the same scope hierarchies, or even multiple invocations of the same function can be active simultaneously

In other words, a working description of PowerShell scope resists simplicity.

The about_Scopes documentation suggests the matter is far simpler than it, in fact, is. Perhaps analysing and understanding the code from this issue would lead to a more complete understanding.

alx9r
  • 3,675
  • 4
  • 26
  • 55
  • I believe the concept you're looking for is "Scope." [link](https://technet.microsoft.com/en-us/library/hh847849.aspx) – Booga Roo Mar 02 '15 at 21:29
  • You can create the variable in the global scope when you create it with `$global:a1 = 10` You've already found "When you use the call operator (&) to run a function or script, it is not added to the current scope" and "To add a function to the current scope, type a dot (.) and a space before the path and name of the function in the function call." You can also use Set-Variable to set scope after creating the variable or New-Variable to set scope at creation time. [Another post about Scopes](http://blogs.msdn.com/b/powershell/archive/2007/04/14/controlling-the-scope-of-variables.aspx) – Booga Roo Mar 02 '15 at 21:36
  • @BoogaRoo Scope is already in the tags for this question. I don't follow how using `.` or `Set-Variable` helps to selectively capture variables in the context of a scriptblock. Do you have an example demonstrating what you mean? – alx9r Mar 02 '15 at 21:44
  • For anyone viewing this in 2022, when running Powershell in Visual Studio Code 1.64.0 with Powershell 7.2.1, the function of these scopes appears to have changed such that the result is 111, incorporating the f1 and f2 scopes when executing the x0 scriptblock inside of f2 even though it was declared outside of both. Executing `f1 $x0.GetNewClosure()` at the command prompt results in 1, reflecting that the closure was created outside the scopes f1 and f2 scopes. – bwerks Feb 16 '22 at 20:54

1 Answers1

1

I was hoping there was a built-in way of achieving this. The closest thing I found was [scriptblock]::InvokeWithContext(). Handling the parameters for InvokeWithContext() manually gets pretty messy. I managed to encapsulate the mess by defining a couple of helper functions in another module:

function ConvertTo-xScriptblockWithContext{
    param([parameter(ValueFromPipeline=$true)]$InputObject)
    process{
        $InputObject | Add-Member -NotePropertyMembers @{variablesToDefine=@()}
        {$InputObject.InvokeWithContext(@{},$InputObject.variablesToDefine)}.GetNewClosure() |
        Add-Member -NotePropertyMembers @{ScriptBlockWithContext=$InputObject} -PassThru
}}  

function Add-xVariableToContext{
    param(
        [parameter(ValueFromPipeline=$true)]$InputObject,
        [parameter(position=1)]$Name,     
        [parameter(position=2)]$Value
    )
    process{
        $exists = $InputObject.ScriptBlockWithContext.variablesToDefine | ? { $_.Name -eq $Name }
        if ($exists) { $exists = $Value }
        else{ $InputObject.ScriptBlockWithContext.variablesToDefine += New-Object 'PSVariable' @($Name,$Value) }
}}

Then, f1 and f2 add variables to the scriptblock's context using Add-xVariableToContext as it passes through:

function f1{
    param($x1)
    $a1 = 10
    $x1 | Add-xVariableToContext 'a1' $a1
    f2 $x1
}

function f2{
    param($x2)
    $a2 = 100
    $x2 | Add-xVariableToContext 'a2' $a2
    & $x2
}

Notice that $x2 is invoked like any other scriptblock so it can be safely used with the variables added to its context by anything that accepts scriptblocks. Creating new scriptblocks, adding $a0 to their context, and passing them to f1 looks like this:

$a0 = 1
$x0a,$x0b = {$a0+$a1+$a2},{$a0*$a1*$a2} | ConvertTo-xScriptblockWithContext
$x0a,$x0b | Add-xVariableToContext 'a0' $a0
f1 $x0a
f1 $x0b
#111
#1000
alx9r
  • 3,675
  • 4
  • 26
  • 55