alx9r
alx9r

Reputation: 4273

To what session state does the script block in [scriptblock]::Create().GetSteppablePipeline() get bound?

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:

  1. $a is accessible both ways -ScriptBlock is invoked. That is because both the functions and -ScriptBlock are bound to the same SessionState. Both functions have parameter $a in the function's scope. That scope becomes an ancestor scope to -ScriptBlock when it is invoked so $a is visible from -ScriptBlock.
  2. $__a is accessible when -ScriptBlock is invoked by Invoke-Call. This is a similar situation to (1). When the scriptblock from [scriptblock]::Create() is invoked it is bound to the SessionState of Invoke-Call which is the same SessionState as -ScriptBlock. So $a is visible from -ScriptBlock.
  3. $__a is not accessible when -ScriptBlock is invoked by Invoke-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?

Upvotes: 1

Views: 51

Answers (1)

alx9r
alx9r

Reputation: 4273

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.

Upvotes: 0

Related Questions