Last active
December 8, 2018 14:14
-
-
Save nohwnd/3ca1c5f2240ee187303867ddb7422665 to your computer and use it in GitHub Desktop.
Invoking scriptblocks with context
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # InvokeWith context is useful when we have something like | |
| # ``` | |
| # $num = 1 | |
| # 1,2,3 | Assert-All -filter { $num -eq $_ } | |
| # ``` | |
| # Where Assert-All is defined in a module where we invoke the filter script. The script needs to | |
| # stay bounded to the original scope to keep $name resolvable, but we also need to push the | |
| # $_ variable into the script block when invoking it. | |
| $m = New-Module -Name abc -ScriptBlock { | |
| function Invoke-WithContext { | |
| param( | |
| [Parameter(Mandatory = $true )] | |
| [ScriptBlock] $ScriptBlock, | |
| [Parameter(Mandatory = $true)] | |
| [hashtable] $Variables) | |
| # this functions is a psv2 compatible version of | |
| # ScriptBlock InvokeWithContext that is not available | |
| # in that version of PowerShell | |
| # this is what the code below does | |
| # which in effect sets the context without detaching the | |
| # scriptblock from the original scope | |
| # & { | |
| # # context | |
| # $a = 10 | |
| # $b = 20 | |
| # # invoking our original scriptblock | |
| # & $sb | |
| # } | |
| # a similar solution was $SessionState.PSVariable.Set('a', 10) | |
| # but that sets the variable for all "scopes" in the current | |
| # scope so the value persist after the original has run which | |
| # is not correct, | |
| $scriptBlockWithContext = { | |
| param($___context) | |
| foreach ($pair in $___context.Variables.GetEnumerator()) { | |
| New-Variable -Name $pair.Key -Value $pair.Value | |
| } | |
| # this cleans up the variable from the session | |
| # the subexpression outputs the value of the variable | |
| # and then deletes the variable, so the value is still passed | |
| # but the variable no longer exists when the scriptblock executes | |
| & $($___context.ScriptBlock; Remove-Variable -Name '___context' -Scope Local) | |
| } | |
| $flags = [System.Reflection.BindingFlags]'Instance,NonPublic' | |
| $SessionState = $ScriptBlock.GetType().GetProperty("SessionState", $flags).GetValue($ScriptBlock, $null) | |
| $SessionStateInternal = $SessionState.GetType().GetProperty('Internal', $flags).GetValue($SessionState, $null) | |
| # attach the original session state to the wrapper scriptblock | |
| # making it invoke in the same scope as $ScriptBlock | |
| $scriptBlockWithContext.GetType().GetProperty('SessionStateInternal', $flags).SetValue($scriptBlockWithContext, $SessionStateInternal, $null) | |
| & $scriptBlockWithContext @{ ScriptBlock = $ScriptBlock; Variables = $Variables } | |
| } | |
| } | |
| Get-Module abc, d | Remove-Module | |
| $m | Import-Module | |
| $a = $null | |
| $b = 11 | |
| $sb = { "-$a- -$b-" } | |
| # it is important that both the variables bound from the original scope | |
| # and the variables in the context are correctly resolved when the scriptblock | |
| # is invoked inside of the module above, this can only be achieved by not unbinding the | |
| # script from the original scope, so the middle line reads -10- -11- in the output. Also | |
| # the $a variable must not be persisted after the scriptblock has run so both the first and | |
| # last line should read "-- -11-" | |
| "-$a- -$b-" | |
| Invoke-WithContext $sb -Variables @{ A = 10 } | |
| "-$a- -$b-" | |
| $mm = New-Module -Name d -ScriptBlock { | |
| $b = 100 | |
| function Get-ScriptBlock { | |
| { "-$a- -$b-" } | |
| } | |
| } | |
| Get-Module d | Remove-Module | |
| $mm | Import-Module | |
| $sb = Get-ScriptBlock | |
| # here we get the scriptblock from the module | |
| # and invoke it, the next line prints "-10- -100-" because | |
| # it is bound to the internals of the module, showing that we | |
| # don't rely on the caller scope as I did in my original solution | |
| # $PSCmdlet.SessionState.PSVariable.Set('a', 10) wher the caller state | |
| # would be modified, not the d module state | |
| Invoke-WithContext $sb -Variables @{ A = 10 } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment