Skip to content

Instantly share code, notes, and snippets.

@nohwnd
Last active December 8, 2018 14:14
Show Gist options
  • Select an option

  • Save nohwnd/3ca1c5f2240ee187303867ddb7422665 to your computer and use it in GitHub Desktop.

Select an option

Save nohwnd/3ca1c5f2240ee187303867ddb7422665 to your computer and use it in GitHub Desktop.
Invoking scriptblocks with context
# 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