'Is there a way to selectively capture variables in a scriptblock's context inside modules?
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, andendscriptblocks 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.
Solution 1:[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
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
