Expand variable with scriptblock inside variable i

2019-07-11 07:31发布

问题:

$RunspaceCollection = @()
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = @({'somecode'},{'someothercode'})

Foreach ($test in $case) {

    $finalcode= {
        Invoke-Command -ScriptBlock [scriptblock]::create($code[$test])
    }.GetNewClosure()

    $Powershell = [PowerShell]::Create().AddScript($finalcode)
    $Powershell.RunspacePool = $RunspacePool
    [Collections.Arraylist]$RunspaceCollection += New-Object -TypeName PSObject -Property @{
        Runspace = $PowerShell.BeginInvoke()
        PowerShell = $PowerShell
}}

The finalcode variable doesn't expand when the GetNewClosure() happens, so $code[$test] gets into the runspace instead of actual code and I can't get my desired results. Any advice?

Using the method from the answer I'm getting this in the runspace, but it doesn't execute properly. I can confirm that my command is loaded into runspace (at least while in debugger inside runspace I can execute it without dot sourcing)

[System.Management.Automation.PSSerializer]::Deserialize('<ObjsVersion="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <SBK> My-Command -Par1 "egweg" -Par2 "qwrqwr" -Par3 "wegweg"</SBK>
</Objs>')

This is what I see in debugger in runspace

Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> 

Stopped at: $a = Invoke-Command -ScriptBlock { $([System.Management.Automation.PSSerializer]::Deserialize('<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
[DBG]: [Process:8064]: [Runspace12]: PS C:\git\infrastructure_samples>> s

Stopped at: </Objs>')) }

回答1:

The problem with your code is that AddScript method of PowerShell class is expecting a string, not ScriptBlock. And any closure will be lost when you convert ScriptBlock to string. To solve this, you can pass argument to script with AddArgument method:

$RunspaceCollection = New-Object System.Collections.Generic.List[Object]
$RunspacePool = [RunspaceFactory]::CreateRunspacePool(1,5)
$RunspacePool.Open()
$code = @({'somecode'},{'someothercode'})
$finalcode= {
    param($Argument)
    Invoke-Command -ScriptBlock ([scriptblock]::create($Argument))
}

Foreach ($test in $case) {
    $Powershell = [PowerShell]::Create().AddScript($finalcode).AddArgument($code[$test])
    $Powershell.RunspacePool = $RunspacePool
    $RunspaceCollection.Add((New-Object -TypeName PSObject -Property @{
        Runspace = $PowerShell.BeginInvoke()
        PowerShell = $PowerShell
    }))
}


回答2:

I'm not sure if there's a better way off the top of my head, but you can replace the variables yourself with serialized versions of the same.

You can't use $Using: in this case, but I wrote a function that replaces all $Using: variables manually.

My use case was with DSC, but it would work in this case as well. It allows you to still write your script blocks as scriptblocks (not as strings), and supports variables with complex types.

Here's the code from my blog (also available as a GitHub gist):

function Replace-Using {
[CmdletBinding(DefaultParameterSetName = 'AsString')]
[OutputType([String], ParameterSetName = 'AsString')]
[OutputType([ScriptBlock], ParameterSetName = 'AsScriptBlock')]
param(
    [Parameter(
        Mandatory,
        ValueFromPipeline
    )]
    [String]
    $Code ,

    [Parameter(
        Mandatory,
        ParameterSetName = 'AsScriptBlock'
    )]
    [Switch]
    $AsScriptBlock
)

    Process {
        $cb = {
            $m = $args[0]
            $ser = [System.Management.Automation.PSSerializer]::Serialize((Get-Variable -Name $m.Groups['var'] -ValueOnly))
            "`$([System.Management.Automation.PSSerializer]::Deserialize('{0}'))" -f $ser
        }
        $newCode = [RegEx]::Replace($code, '\$Using:(?<var>\w+)', $cb, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)

        if ($AsScriptBlock.IsPresent) {
            [ScriptBlock]::Create($newCode)
        } else {
            $newCode
        }
    }
}

A better way for me to do this replacement would probably be to use the AST instead of string replacement, but.. effort.

Your Use Case

$finalcode= {
    Invoke-Command -ScriptBlock [scriptblock]::create($Using:code[$Using:test])
} | Replace-Using

For better results you might assign a variable first and then just insert that:

$value = [scriptblock]::Create($code[$test])

$finalcode= {
    Invoke-Command -ScriptBlock $Using:value
} | Replace-Using