Jason Shirk wrote
A script block is bound to a SessionState immediately if you use the { … } syntax, or upon the first invocation if the script block was created some other way, e.g. [ScriptBlock]::Create(). The binding is to the active SessionState.
Indeed that seems to be correct. This thread contemplates that behavior extensively. For reference, here is an annotated example exercising the interactions between PowerShell scopes, . and &, modules, and SessionStates.
Consider the following code in which two instances of identical scriptblocks produced by [scriptblock]::Create() are invoked using the call operator & and SteppablePipeline, respectively:
function Invoke-Call {
[CmdletBinding()]
param(
[scriptblock]
$ScriptBlock,
$a
)
& ([scriptblock]::Create({ param ($sb,$__a) . $sb $__a })) <# This scriptblock gets bound to the SessionState that is active when the call operator is invoked. #> `
$ScriptBlock $a
}
function Invoke-Steppable {
[CmdletBinding()]
param(
[scriptblock]
$ScriptBlock,
$a
)
$pipe = [scriptblock]::Create({ param ($sb,$__a) . $sb $__a }). # To what SessionState does this scriptblock get bound?
GetSteppablePipeline(
$MyInvocation.CommandOrigin,
@($ScriptBlock,$a))
$pipe.Begin($PSCmdlet)
$pipe.End()
}
Invoke-Call -a 'c' -ScriptBlock { param($x) [pscustomobject]@{function = 'Invoke-Call'; x=$x; a=$a; __a=$__a }}
Invoke-Steppable -a 's' -ScriptBlock { param($x) [pscustomobject]@{function = 'Invoke-Steppable'; x=$x; a=$a; __a=$__a }}
That code outputs
function x a __a
-------- - - ---
Invoke-Call c c c
Invoke-Steppable s s
The following, I think, is noteworthy about that output:
$ais accessible both ways-ScriptBlockis invoked. That is because both the functions and-ScriptBlockare bound to the sameSessionState. Both functions have parameter$ain the function’s scope. That scope becomes an ancestor scope to-ScriptBlockwhen it is invoked so$ais visible from-ScriptBlock.$__ais accessible when-ScriptBlockis invoked byInvoke-Call. This is a similar situation to (1). When the scriptblock from[scriptblock]::Create()is invoked it is bound to theSessionStateofInvoke-Callwhich is the sameSessionStateas-ScriptBlock. So$ais visible from-ScriptBlock.$__ais not accessible when-ScriptBlockis invoked byInvoke-Steppable.
This suggests the script block from [scriptblock]::Create() that is invoked by SteppablePipeline is not bound to the same SessionState as -ScriptBlock.
To what session state does that script block get bound?
2
TL;DR
None of the testing described below suggests anything unusual about the binding of the ScriptBlock in [scriptblock]::Create().GetSteppablePipeline(): It seems to behave as though it were bound to the same SessionState as the command whose whose reference is passed to the SteppablePipeline methods. In fact, Set-Variable works exactly as expected for local variables that aren’t in param().
Script block binding doesn’t seem to explain why param() variables aren’t visible to descendant scopes.
Direct Observation of ScriptBlock.SessionStateInternal
Consider the script binding.ps1 in this reference code which creates and invokes script block $sb using the methods
# call
& $sb
# steppable
. {
[CmdletBinding()]param()
$pipe = $sb.GetSteppablePipeline($MyInvocation.CommandOrigin,@())
$pipe.Begin($PSCmdlet)
$pipe.End()
}
and extracts SessionStateInternal from each script block before and after invokation using the function getSessionState. That script produces the following results:
method provenance same before after
------ ---------- ---- ------ -----
call {} True yes yes
call [scriptblock]::Create()
call {}.Ast.GetScriptBlock()
steppable {} True yes yes
steppable [scriptblock]::Create()
steppable {}.Ast.GetScriptBlock()
same in the table represents a reference-equality check of the before and after SessionStateInternal.
So SteppablePipeline does not seem to alter SessionStateInternal of the script block from which it is derived.
SessionStateInternal of Nested ScriptBlock
By the same the principle as the previous section the script block
([scriptblock]::Create({ . { getSessionState {} }}))
invoked using SteppablePipeline reveals SessionStateInternal for the nested {}. The script steppableSessionState.ps1 in the reference code, does that and compares that SessionStateInternal instance with others that are easily obtained. The instance of SessionStateInternal from the above line invoked with SteppablePipeline matches others according to the following table:
Other SessionStateInternal |
Matches |
|---|---|
{ . { getSessionState {} }} invoked with SteppablePipeline |
yes |
{} defined locally |
yes |
{} defined in a module |
no |
So far none of these results suggest anything unusual about the binding of the ScriptBlock in [scriptblock]::Create().GetSteppablePipeline(): It seems to behave as though it were bound to the same SessionState as the command whose whose reference is passed to the SteppablePipeline methods.
Set-Variable
If there is a session state associated with the ScriptBlock in [scriptblock]::Create().GetSteppablePipeline(), then it should be possible to set a variable there and that variable should be visible from descendent scopes. Indeed, the code
$invoke = {
[CmdletBinding()]param(
$SubjectScriptBlock
)
$pipe = $SubjectScriptBlock.GetSteppablePipeline($MyInvocation.CommandOrigin,@())
$pipe.Begin($PSCmdlet)
$pipe.End()
}
. $invoke { . { begin { Set-Variable y why} } | & { $y }}
outputs why. So there is a SessionState that behave as normal with respect to setting local variables. But the very similar
$invoke = {
[CmdletBinding()]param(
$SubjectScriptBlock,
$a
)
$pipe = $SubjectScriptBlock.GetSteppablePipeline($MyInvocation.CommandOrigin,@($a))
$pipe.Begin($PSCmdlet)
$pipe.End()
}
. $invoke { param($__a) & { $__a }} -a aye
outputs nothing. None of the above seems to account for that difference.
