24

I'm trying to create a background job, which executes a scriptblock. I need to pass this scriptblock in as a parameter, but I can't seem to get the syntax to work. The scriptblock is being converted to a string somewhere along the way.

It works fine when I pass the script block to a local function, but not through start-job

The following syntax works:

function LocalFunction
{
    param (
        [parameter(Mandatory=$true)]
        [ScriptBlock]$ScriptBlock
    )

    &$ScriptBlock | % { echo "got $_" }
}

LocalFunction -ScriptBlock { echo "hello" }

This outputs "got hello" as expected.

But the following fails:

$job = start-job -argumentlist { echo "hello" } -scriptblock {
    param (
        [parameter(Mandatory=$true)]
        [ScriptBlock]$ScriptBlock
    )
    &$ScriptBlock | % { echo "got $_" }
}
start-sleep -s 1
receive-job $job

The error it returns is

Receive-Job : Cannot process argument transformation on parameter 'ScriptBlock'. Cannot convert the " echo "hello" " value of type "System.String" to type "System.Management.Automation.ScriptBlock".

So if I'm reading the error right, it appears that -argumentlist is somehow forcing its arguments into strings.

Kalle Richter
  • 8,008
  • 26
  • 77
  • 177
ben
  • 1,441
  • 2
  • 16
  • 21
  • Im facing this same issue while using `psake`. I created a precondition `$prec = { ... }` and I tried to pass this same precondition to various tasks. I get the same error which you have. Let me know if you find a solution.. – Zasz Sep 03 '12 at 16:24

5 Answers5

21

Here's one way to solve this, pass the scriptblock code as a string, then create a scriptblock from the string inside the job and execute it

Start-Job -ArgumentList "write-host hello"  -scriptblock {

    param (
        [parameter(Mandatory=$true)][string]$ScriptBlock
    )

    & ([scriptblock]::Create($ScriptBlock))

} | Wait-Job | Receive-Job
Shay Levy
  • 121,444
  • 32
  • 184
  • 206
  • 3
    This doesn't answer the question of how to pass a script-block, and I need to do that as well. – Zasz Sep 03 '12 at 16:30
  • 2
    No, you're right, it doesn't answer the question directly but it gives a workaround. As far as I can tell, parameters are serialized (just like in remoting), and scriptblocks only go across as strings. The workaround is to use the ScriptBlock.Create method. – Shay Levy Sep 04 '12 at 19:29
  • Thanks. Ill use strings to pass around code. I gather from what you say : powershell will always serialize arguments, even when not using network, while just passing them from one method to another. – Zasz Sep 05 '12 at 04:23
  • 2
    Just to clarify, PowerShell serializes arguments for Job and Remoting cmdlets. – Shay Levy Sep 05 '12 at 09:17
  • 1
    Great workaround; that script blocks deserialize to strings is by (surprising) design: "Historically this is by design. Serializing scriptblocks with fidelity resulted in too many places where there was automatic code execution so to facilitate secure restricted runspaces, scriptblocks are always deserialized to strings." - https://github.com/PowerShell/PowerShell/issues/4218#issuecomment-314851921 – mklement0 Jul 16 '17 at 18:21
6

Looks like this works today.

function LocalFunction
{
    param
    (
        [scriptblock] $block
    )

    $block.Invoke() | % { "got $_" }
}

LocalFunction { "Hello"} 
awright18
  • 2,255
  • 2
  • 23
  • 22
  • 1
    I think this has always worked, because there are no _jobs_ (or remoting) involved (as stated, the problem is a result of the _serialization_ that is implicitly involved when creating jobs / remoting). As an aside, the more idiomatic way to invoke a script block is to use the `&` operator (or, if you don't want to create a child scope for variables, the `.` operator). – mklement0 Jul 16 '17 at 18:06
4

Based on my experiments, PowerShell is parsing -ArgumentList, which is an Object[], as a string, even when you pass in a script block. The following code:

$job = start-job -scriptblock { $args[0].GetType().FullName } -argumentlist { echo "hello" }
start-sleep -s 1
receive-job $job

results in the following output:

System.String

As far as I know, the only solution here is Shay's, though you don't need to pass in the -ArgumentList as a string as PowerShell will parse your script block as a string in this case.

James Kovacs
  • 11,549
  • 40
  • 44
  • Nice demonstration of the problem, but, strictly speaking, the problem is not not how `-ArgumentList` values _parses_ script- blocks, it is that they are converted to strings _on deserialization_ (which, as Shay, points out, is implicitly involved when creating jobs / doing remoting). – mklement0 Jul 16 '17 at 18:25
1

You have to read it in as a string and then convert it to a scriptblock.

In powershell v1 you can do this:

$ScriptBlock = $executioncontext.invokecommand.NewScriptBlock($string)

And in powershell v2 you can do this:

$ScriptBlock = [scriptblock]::Create($string)

So your code would look like this:

function LocalFunction
{
    param (
        [parameter(Mandatory=$true)]
        $ScriptBlock
    )

    $sb = [scriptblock]::Create($ScriptBlock)

    $sb | % { echo "got $_" }
}

LocalFunction -ScriptBlock "echo 'hello'"

The '[scriptblock]::Create($ScriptBlock)' will place the curly braces around the string for you creating the script block.

Found the info here http://get-powershell.com/post/2008/12/15/ConvertTo-ScriptBlock.aspx

1

So if your desire is to insert an inline scriptblock, then Shay's solution (as noted) is probably the best. On the other hand if you simply want to pass a scriptblock as a parameter consider using a variable of type scriptblock and then passing that as the value of the -ScriptBlock parameter.

function LocalFunction
{
    param (
        [parameter(Mandatory=$true)]
        [ScriptBlock]$ScriptBlock
    )

    &$ScriptBlock | % { echo "got $_" }
}

[scriptblock]$sb = { echo "hello" }

LocalFunction -ScriptBlock $sb
Sled
  • 18,541
  • 27
  • 119
  • 168
jrobiii
  • 101
  • 3