functionpowershellscopepipelinescriptblock

How can I invoke a scriptblock in the caller's context?


Consider the following call site:

$modifiedLocal = 'original local value'
'input object' | SomeScriptblockInvoker {
    $modifiedLocal = 'modified local value'
    [pscustomobject] @{
        Local = $local
        DollarBar = $_
    }
}
$modifiedLocal

I would like to implement SomeScriptblockInvoker such that

  1. it is defined in a module, and
  2. the scriptblock is invoked in the caller's context.

The output of the function at the call site would be the following:

Local DollarBar   
----- ---------   
local input object
modified local value

PowerShell seems to be capable of doing this. For example replacing SomeScriptblockInvoker with ForEach-Object yields exactly the desired output.

I have come close using the following definition:

New-Module m {
    function SomeScriptblockInvoker {
        param
        (
            [Parameter(Position = 1)]
            [scriptblock]
            $Scriptblock,

            [Parameter(ValueFromPipeline)]
            $InputObject
        )
        process
        {
            $InputObject | . $Scriptblock
        }
    }
} |
    Import-Module

The output of the call site using that definition is the following:

Local DollarBar
----- ---------
local          
modified local value

Note that DollarBar is empty when it should be input object.

(gist of Pester tests to check for correct behavior)


Solution

  • In general, you can't. The caller of a scriptblock does not have control over the SessionState associated with that scriptblock and that SessionState determines (in part) the context in which a scriptblock is executed (see the Scope section for details). Depending on where the scriptblock is defined, its SessionState might match the caller's context, but it might not.

    Scope

    With respect to the context in which the scriptblock is executed, there are two related considerations:

    1. The SessionState associated with the scriptblock.
    2. Whether or not the calling method adds a scope to the SessionState's scope stack.

    Here is a good explanation of how this works.

    The $_ Automatic Variable

    $_ contains the current object in the pipeline. The scriptblock provided to % is interpreted differently from the scriptblock provided . and &:

    Note that in the OP $_ was empty when the scriptblock was invoked using .. This is because the scriptblock contained no process{} block. Each of the statements in the scriptblock were implicitly part of the scriptblock's end{} block. By the time an end{} block is run, there is no longer any object in the pipeline and $_ is null.

    . vs & vs %

    ., &, and % each invoke the scriptblock using the SessionState of the scriptblock with some differences according to the following table:

    +---+-----------------+-----------+-------------------+----------------------+
    |   |      Name       |    Kind   |  interprets {} as |  adds scope to stack |
    +---+-----------------+-----------+-------------------+----------------------+
    | % |  ForEach-Object |  Command  |  Process block    |  No                  |
    | . |  dot-source     |  Operator |  scriptblock      |  No                  |
    | & |  call           |  Operator |  scriptblock      |  Yes                 |
    +---+-----------------+-----------+-------------------+----------------------+
    

    The Most Viable Options

    The two most viable options for passing the tests in OP are as follows. Note that neither invokes the scriptblock strictly in the caller's context but rather in a context using the SessionState associated with the scriptblock.

    Option 1

    Change the call site so that the scriptblock includes process{}:

    $modifiedLocal = 'original local value'
    'input object' | SomeScriptblockInvoker {
        process {
            $modifiedLocal = 'modified local value'
            [pscustomobject] @{
                Local = $local
                DollarBar = $_
            }
        }
    }
    $modifiedLocal
    

    And invoke the scriptblock using SomeScriptblockInvoker in OP.

    Option 2

    Invoke the scriptblock using % as suggested by PetSerAl.