7

I want to create a PowerShell function that enumerates some data, and fire a script block on all occurrences.

By now I have (this is not the actual code, but it illustrates my issue):

function Invoke-TenTimes
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [ScriptBlock]$Action
    )
    process
    {
        $digits = 0..10
        $digits | % {
            $Action.Invoke($_);
        }
    }
}

I put this function in my module. However, I don't get any result when I call:

Invoke-TenTimes { $_ }

The output is blank (nothing is displayed).

If I call

Invoke-TenTimes { $_ -eq $null }

I get ten true. In fact, I see that $_ is null.

What is the proper way to populate the $_ ?

What is driving me crazy, is that if I put this function and the call in the same ps1 file, it works (but I want to pass script block on demand):

function Invoke-TenTimes
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory=$true, Position=0)]
        [ScriptBlock]$Action
    )
    process
    {
        $digits = 0..10
        $digits | % {
            $Action.Invoke($_);
        }
    }
}


Invoke-TenTimes { $_ }
Steve B
  • 36,818
  • 21
  • 101
  • 174

4 Answers4

5

Here we go:

$scriptblock = {param($number) Write-Host "Current Number is $number"} 

function Invoke-TenTimes
{
    [CmdletBinding()]
    param
    (
       [Parameter(Mandatory=$true, Position=0)]
       [ScriptBlock]$Action
    )
    $digits = 1..10
    $digits | ForEach-Object{Invoke-Command -ScriptBlock $Action -Args $_}
 }

Use:

Invoke-TenTimes $scriptblock
3

The issue is known and it is specific to modules. Perhaps this is even a bug. Please take a look at this question: Strange behavior with Powershell scriptblock variable scope and modules, any suggestions?

As for your particular case, call your function like this:

Invoke-TenTimes { $args[0] }

In this way it should work as expected.

Community
  • 1
  • 1
Roman Kuzmin
  • 40,627
  • 11
  • 95
  • 117
  • This solved my problem. But I'll wait a few days to see if someone has an actual solution to suggest. – Steve B Oct 02 '12 at 12:46
2

this is a scoping issue with the passed in scriptblock belonging to a different scope than where it is executing. If the passed in scriptblock won't have to refer to variables from its context you can simply clone the scriptblock to force it to be in the scope you are running it. with

$action = [scriptblock]::Create($action)

the interesting thing is it only worked in the first place by ACCIDENT. it was getting the $_ from the parent scope of where the scriptblock belonged which happened to be inside you foreach-object which was good.

however take your original version, and then run this

"gotcha" | % {
Invoke-TenTimes {  $_ }
}

and you'll see you'll get gotcha 10 times, because $_ is not null in your callers scope, but is something.

klumsy
  • 4,081
  • 5
  • 32
  • 42
0

It's MUCH easier than everyone lets on.

Consider $A and $B are defined at the global level and only $B is defined at the module level. Your script block is a 3rd scope. Within it you refer to $A and get the global value. Similarly if you refer to $B you get the module level value since it overshadows the global instance.

PROBLEM: If you try to write to either - a local instance is created.

Answer:

([ref]$A).value = 'Something malicious'

Note that "([ref]${name})" is a reference object so ".value" is required to reach the actual variable. So

([ref]$B).value.Length

is the same as

$B.Length

In either case you have no direct control over scope. You get the instance you would read from. So the first example modifies the Global $A and the second example references the module level instance of $B.

bielawski
  • 1,466
  • 15
  • 20