63

I guess you can't just do this:

  $servicePath = $args[0]

  if(Test-Path -path $servicePath) <-- does not throw in here

  $block = {

        write-host $servicePath -foreground "magenta"

        if((Test-Path -path $servicePath)) { <-- throws here.

              dowork 
        }
  }

So how can I pass my variables to the scriptblock $block?

Lars Truijens
  • 42,837
  • 6
  • 126
  • 143
dexter
  • 7,063
  • 9
  • 54
  • 71

8 Answers8

68

Keith's answer also works for Invoke-Command, with the limit that you can't use named parameters. The arguments should be set using the -ArgumentList parameter and should be comma separated.

$sb = {
    param($p1,$p2)
    $OFS=','
    "p1 is $p1, p2 is $p2, rest of args: $args"
}
Invoke-Command $sb -ArgumentList 1,2,3,4

Also see here and here.

jpaugh
  • 6,634
  • 4
  • 38
  • 90
Lars Truijens
  • 42,837
  • 6
  • 126
  • 143
42

A scriptblock is just an anonymous function. You can use $args inside the scriptblock as well as declare a param block, for example

$sb = {
  param($p1, $p2)
  $OFS = ','
  "p1 is $p1, p2 is $p2, rest of args: $args"
}
& $sb 1 2 3 4
& $sb -p2 2 -p1 1 3 4
participant
  • 2,923
  • 2
  • 23
  • 40
Keith Hill
  • 194,368
  • 42
  • 353
  • 369
  • Right, but don't closures traditionally capture variables as well? I'd assume that $servicePath above would be captured. – Kakira Sep 15 '14 at 21:06
  • 8
    Not in PowerShell. If you run the scriptblock in the current runspace, then yeah, those variables are picked up. But that is just a dynamic scoping feature. Try this with the scriptblock for Start-Job, where the scriptblock is serialized to another PowerShell process for execution and you will see no automatically captured variables. – Keith Hill Sep 15 '14 at 21:38
  • If I wanted to run this same function as a new on-screen Powershell instance, is there any way to pass the arguments in? I'd have thought it would just be something like `start powershell $sb(1,2)`. – Steve can help Jul 20 '21 at 08:42
15

For anyone reading in 2020 who wants to use local variables in a remote session script block, starting in Powershell 3.0 you can use local variables directly in the scriptblock with the "$Using" scope modifier. Example:

$MyLocalVariable = "C:\some_random_path\"
acl = Invoke-Command -ComputerName REMOTEPC -ScriptBlock {Get-Acl $Using:MyLocalVariable}

Found in example 9 of https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/invoke-command?view=powershell-7

user724067
  • 151
  • 1
  • 3
13

BTW, if using the script block to run in a separate thread (multi threaded):

$ScriptBlock = {
    param($AAA,$BBB) 
    return "AAA is $($AAA) and BBB is $($BBB)"
}

$AAA = "AAA"
$BBB = "BBB1234"    
$null = Start-Job $ScriptBlock -ArgumentList $AAA,$BBB

then yields:

$null = Start-Job $ScriptBlock -ArgumentList $AAA,$BBB    
Get-Job | Receive-Job
AAA is AAA and BBB is BBB1234
4

By default PowerShell won't capture variables for a ScriptBlock. You can explicitly capture by calling GetNewClosure() on it, however:

$servicePath = $args[0]

if(Test-Path -path $servicePath) <-- does not throw in here

$block = {

    write-host $servicePath -foreground "magenta"

    if((Test-Path -path $servicePath)) { <-- no longer throws here.

          dowork 
    }
}.GetNewClosure() <-- this makes it work
Chris R. Donnelly
  • 3,086
  • 3
  • 28
  • 28
  • Note that `GetNewClosure()` doesn't appear to work with `Start-Job`. – Ian Kemp Apr 13 '18 at 13:27
  • @IanKemp Interesting, do you know any more as to why it doesn't? Does it fail as the original question describes, or with a different error? – Chris R. Donnelly Apr 13 '18 at 15:01
  • 2
    @ChrisRDonnelly Seems that the variables' values simply don't get copied into the variables in the ScriptBlock, so those inside the block end up having the default value (null). – Ian Kemp Apr 13 '18 at 18:21
  • 1
    It does only work with Global variables. If you have a script block and another script block inside it, the variable defined inside first - parent script block will become Local and GetNewClosure() will not capture and make it available inside the second - child script block. Need to make it Global, e.g.: `function parentScriptBlock(myParam) { myVar = 3; $Global:myVar = $myVar; $Global:myParam = $myParam; childScriptBlock = { echo $myParam }.GetNewClosure(); # use the script block here }` – papo Sep 08 '20 at 13:21
2

Three example syntax:

$a ={ 
  param($p1, $p2)
  "p1 is $p1"
  "p2 is $p2"
  "rest of args: $args"
}
//Syntax 1:
Invoke-Command $a -ArgumentList 1,2,3,4 //PS> "p1 is 1, p2 is 2, rest of args: 3 4"
//Syntax 2:
&$a -p2 2 -p1 1 3      //PS> "p1 is 1, p2 is 2, rest of args: 3"
//Syntax 3:
&$a 2 1 3              //PS> "p1 is 2, p2 is 1, rest of args: 3"
schen
  • 107
  • 1
  • 2
  • 7
1

I know this article is a bit dated, but I wanted to throw this out as a possible alternative. Just a slight variation of the previous answers.

$foo = {
    param($arg)

    Write-Host "Hello $arg from Foo ScriptBlock" -ForegroundColor Yellow
}

$foo2 = {
    param($arg)

    Write-Host "Hello $arg from Foo2 ScriptBlock" -ForegroundColor Red
}


function Run-Foo([ScriptBlock] $cb, $fooArg){

    #fake getting the args to pass into callback... or it could be passed in...
    if(-not $fooArg) {
        $fooArg = "World" 
    }
    #invoke the callback function
    $cb.Invoke($fooArg);

    #rest of function code....
}

Clear-Host

Run-Foo -cb $foo 
Run-Foo -cb $foo 

Run-Foo -cb $foo2
Run-Foo -cb $foo2 -fooArg "Tim"
SpaceGhost440
  • 460
  • 4
  • 17
1

Other possibility:

$a ={ 
    param($p1, $p2)
    "p1 is $p1"
    "p2 is $p2"
    "rest of args: $args"
};
$a.invoke(1,2,3,4,5)
Sled
  • 18,541
  • 27
  • 119
  • 168