3

I normally do the following to invoke a script block containing $_:

$scriptBlock = { $_ <# do something with $_ here #> }
$theArg | ForEach-Object $scriptBlock

In effect, I am creating a pipeline which will give $_ its value (within the Foreach-Object function invocation).

However, when looking at the source code of the LINQ module, it defines and uses the following function to invoke the delegate:

# It is actually surprisingly difficult to write a function (in a module)
# that uses $_ in scriptblocks that it takes as parameters. This is a strange
# issue with scoping that seems to only matter when the function is a part
# of a module which has an isolated scope.
# 
# In the case of this code:
# 1..10 | Add-Ten { $_ + 10 }
#
# ... the function Add-Ten must jump through hoops in order to invoke the
# supplied scriptblock in such a way that $_ represents the current item
# in the pipeline.
#
# Which brings me to Invoke-ScriptBlock.
# This function takes a ScriptBlock as a parameter, and an object that will
# be supplied to the $_ variable. Since the $_ may already be defined in
# this scope, we need to store the old value, and restore it when we are done.
# Unfortunately this can only be done (to my knowledge) by hitting the
# internal api's with reflection. Not only is this an issue for performance,
# it is also fragile. Fortunately this appears to still work in PowerShell
# version 2 through 3 beta.
function Invoke-ScriptBlock {
[CmdletBinding()]

    param (
        [Parameter(Position=1,Mandatory=$true)]
        [ScriptBlock]$ScriptBlock,

        [Parameter(ValueFromPipeline=$true)]
        [Object]$InputObject
    )

    begin {
            # equivalent to calling $ScriptBlock.SessionState property:
            $SessionStateProperty = [ScriptBlock].GetProperty('SessionState',([System.Reflection.BindingFlags]'NonPublic,Instance'))
            $SessionState = $SessionStateProperty.GetValue($ScriptBlock, $null)
        }
    }
    process {
        $NewUnderBar = $InputObject
        $OldUnderBar = $SessionState.PSVariable.GetValue('_')
        try {
            $SessionState.PSVariable.Set('_', $NewUnderBar)
            $SessionState.InvokeCommand.InvokeScript($SessionState, $ScriptBlock, @())
        }
        finally {
            $SessionState.PSVariable.Set('_', $OldUnderBar)
        }
    }
}

This strikes me as a bit low-level. Is there a recommended, safe way of doing this?

Tahir Hassan
  • 5,715
  • 6
  • 45
  • 65

1 Answers1

3

You can invoke scriptblocks with the ampersand. No need to use Foreach-Object.

$scriptblock = {## whatever}
& $scriptblock

@(1,2,3) | % { & {write-host $_}}

To pass parameters:

$scriptblock = {write-host $args[0]}
& $scriptblock 'test'

$scriptBlock = {param($NamedParam) write-host $NamedParam}
& $scriptBlock -NamedParam 'test'

If you're going to be using this inside of Invoke-Command, you could also usin the $using construct.

$test = 'test'
$scriptblock = {write-host $using:test}
Adam Bertram
  • 3,858
  • 4
  • 22
  • 28
  • 1
    Please read the question again. I already know that. I am talking about special script-blocks which contain `$_` variable. – Tahir Hassan Mar 09 '16 at 17:16
  • I did read your question and tested it. Regardless of the scriptblock containing the pipeline variable, it works the same. I've updated the answer. – Adam Bertram Mar 09 '16 at 17:19
  • 2
    It will not work, if you have to jump between modules. [link](http://stackoverflow.com/q/33232360) – user4003407 Mar 09 '16 at 17:32
  • 1
    On my computer, `& { $_ } 10` returns nothing but `& { param($x) $x } 10` correctly returns the value 10. I am asking about the recommended invoking of pipeline variable-ed (`$_`) script-blocks. – Tahir Hassan Mar 09 '16 at 17:33
  • 1
    I see. You are asking about passing parameters into scriptblocks? That's a different story altogether. No need to downvote my answer based on your misunderstanding of the terminology. I've updated my answer. – Adam Bertram Mar 09 '16 at 17:35