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
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
.
Much of the discussion in my original answer below remains correct, but, that answer has shortcomings as follows:
SomeScriptblockInvoker
in option 1 below results in binding each input object twice. I think at least one of those parameter bindings is averted using the steppable pipeline.The implemenation below uses both the steppable pipeline and allows for selection of the session state in which to invoke the script block. Santiago's answer already demonstrates an implementation of a function to replace SomeScriptBlockInvoke
using the steppable pipeline. The following script block adds the machinery required to select the session state:
New-Module {
$label = 'invoker session state'
function SomeScriptblockInvoker {
param(
[Parameter(Mandatory)]
[scriptblock]
$ScriptBlock,
[Parameter()]
[ValidateSet('ScriptBlock','this')]
$InSessionState,
[Parameter(ValueFromPipeline)]
$InputObject
)
begin {
$steppable =
switch ($InSessionState) {
'ScriptBlock' {
{ ForEach-Object $ScriptBlock }.GetSteppablePipeline()
}
'this' {
$m = [psmoduleinfo]::new(<# linkToGlobal: #> $false)
$m.SessionState = $PSCmdlet.SessionState
{ . $m {
param($sb) $input |
ForEach-Object $sb.Ast.GetScriptBlock()
} $ScriptBlock
}.GetSteppablePipeline()
}
}
$steppable.Begin($true)
}
process { $steppable.Process($InputObject) }
end { $steppable.End() }
}} |
Import-Module
# module to demonstrate SessionState selection
New-Module {
$label = 'script block origin session state'
function Get-SomeScriptBlock { { "$label $_" } } # script block bound to this module's session state
} |
Import-Module
$sb = Get-SomeScriptBlock
$label = 'invocation call site session state'
1..2 | SomeScriptblockInvoker $sb -InSessionState ScriptBlock
3..4 | SomeScriptblockInvoker $sb -InSessionState this
Invoking that outputs
script block origin session state 1
script block origin session state 2
invocation call site session state 3
invocation call site session state 4
which demonstrates the same script block was invoked with each of the two session states.
Note that the availability of the technique used to achieve -InSessionState this
might be unintentional. That technique was first suggested to me by Patrick Meinecke where he wondered whether this ability was intentional:
...The
SessionState
property is writable (mistake, maybe?)
Jason Shirk concurred that this ability seems unintentional:
I agree...that a writable
SessionState
property inModuleInfo
seems unintentional. Changes to apis are generally conservative, so maybe you'd be safe using it, but I'd continue looking for alternatives so you have options to consider, or even fall back on if the api was removed.
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.
With respect to the context in which the scriptblock is executed, there are two related considerations:
Here is a good explanation of how this works.
$_
Automatic Variable$_
contains the current object in the pipeline. The scriptblock provided to %
is interpreted differently from the scriptblock provided .
and &
:
'input_object' | % {$_}
- The value of $_
is 'input_object'
because the scriptblock is bound to %
's -Process
parameter. That scriptblock is executed once for each object in the pipeline.'input_object' | . {process{$_}}
and 'input_object' | & {process{$_}}
- The value of $_
is 'input_object'
because the $_
in the scriptblock is inside a process{}
block which executes once for each object in the pipeline.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 |
+---+-----------------+-----------+-------------------+----------------------+
%
command has other parameters corresponding to Begin{}
and End{}
blocks.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.
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.
Invoke the scriptblock using %
as suggested by PetSerAl.