Search code examples
powershellscopescriptblock

When is a ScriptBlock not a ScriptBlock?


I do not mean to sound too cute with the question, but that really is the question at hand. Consider the following two functions defined in a PowerShell module Test.psm1 installed under $env:PSModulePath:

function Start-TestAsync
{
    [CmdletBinding()]
    param([ScriptBlock]$Block, [string]$Name = '')   
    Start-Job { Start-Test -Name $using:Name -Block $using:Block }
}

function Start-Test
{
    [CmdletBinding()]
    param([ScriptBlock]$Block, [string]$Name = '')
    # do some work here, including this:
    Invoke-Command -ScriptBlock $Block
}

After importing the module, I can then run the synchronous function...

PS> Start-Test -Name "My Test" -Block { ps | select -first 9 }

...and it displays appropriate output from Get-Process.

However, when I attempt to run the asynchronous version...

PS> $testJob=Start-TestAsync -Name "My Test" -Block { ps | select -first 9 }

...and then review its output...

PS> Receive-Job $testJob

... it fails at just bringing in the parameter to the Start-Test function, reporting it cannot convert a String to a ScriptBlock. Thus, -Block $using:Block is passing a String rather than a ScriptBlock!

After some experimentation, I did find a workaround. If I modify Start-Test so that the type of the $Block parameter is [string] instead of [ScriptBlock] -- and then convert that string back to a block to feed to Invoke-Command...

function Start-Test
{
    [CmdletBinding()]
    param([string]$Block, [string]$Name = '')
    $myBlock = [ScriptBlock]::Create($Block)
    Invoke-Command -ScriptBlock $myBlock
}

I then obtain the correct result when I run the same commands from above:

PS> $testJob=Start-TestAsync -Name "My Test" -Block { ps | select -first 9 }
PS> Receive-Job $testJob

Is the using scope working correctly in my initial example (converting a ScriptBlock to a string)? The limited documentation on it (about_Remote_Variables, about_Scopes) offers little guidance. Ultimately, is there a way to make Start-Test work when its $Block parameter is typed as a [ScriptBlock]?


Solution

  • While it is useful to know (thanks, @KeithHill) that what I was seeing was a known issue--sorry, I meant "by design"--my real question had not been answered ("Ultimately, is there a way to make Start-Test work when its $Block parameter is typed as a [ScriptBlock]?")

    The answer came to me suddenly last night:

    function Start-TestAsync
    {
        [CmdletBinding()]
        param([ScriptBlock]$Block, [string]$Name = '')   
        Start-Job {
            $myBlock = [ScriptBlock]::Create($using:Block);
            Start-Test -Name $using:Name -Block $myBlock  }
    }
    
    function Start-Test
    {
        [CmdletBinding()]
        param([ScriptBlock]$Block, [string]$Name = '')   
        # do some work here, including this:
        Invoke-Command -ScriptBlock $Block
    }
    

    Notice that in Start-TestAsync I internally allow the serialization to occur ($using:Block), converting the ScriptBlock to a String, then immediately re-convert it (Create) to a ScriptBlock, and can then safely pass that on to Start-Test as a genuine ScriptBlock. To me, this is a significant improvement over the workaround in my question because now the public APIs on both functions are correct.