【问题标题】:When is a ScriptBlock not a ScriptBlock?什么时候 ScriptBlock 不是 ScriptBlock?
【发布时间】:2014-09-08 16:15:45
【问题描述】:

我并不是说这个问题听起来太可爱了,但这确实是眼前的问题。考虑在 $env:PSModulePath 下安装的 PowerShell 模块 Test.psm1 中定义的以下两个函数:

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
}

导入模块后,就可以运行同步功能了……

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

...它会显示来自 Get-Process 的适当输出。

但是,当我尝试运行异步版本时...

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

...然后查看其输出...

PS> Receive-Job $testJob

... 仅将参数引入 Start-Test 函数时失败,报告它无法将 String 转换为 ScriptBlock。因此,-Block $using:Block 传递的是 String 而不是 ScriptBlock!

经过一些实验,我确实找到了解决方法。如果我修改 Start-Test 以便 $Block 参数的类型是 [string] 而不是 [ScriptBlock] - 然后将该字符串转换回一个块以提供给 Invoke-Command...

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

当我从上面运行相同的命令时,我会得到正确的结果:

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

using 范围在我的初始示例中是否正常工作(将 ScriptBlock 转换为字符串)?关于它的有限文档(about_Remote_Variablesabout_Scopes)提供的指导很少。 最终,当 Start-Test 的 $Block 参数键入为 [ScriptBlock] 时,有没有办法让 Start-Test 工作?

【问题讨论】:

    标签: powershell scope scriptblock


    【解决方案1】:

    这显然是设计使然:https://connect.microsoft.com/PowerShell/feedback/details/685749/passing-scriptblocks-to-the-job-as-an-argument-cannot-process-argument-transformation-on-parameter

    解决方法(来自上面的链接)是使用[ScriptBlock]::Create()

    发生这种情况是因为 $ScriptToNest 脚本块正在转换为字符串 因为 PowerShell 序列化的工作方式。您可以通过显式解决此问题 创建脚本块。将 $OuterScriptblock 中的 param() 块替换为 以下($ip 是输入):

    [scriptblock]$OuterScriptblock = {
    param($ip)
    [ScriptBlock]$ScriptToRun = [ScriptBlock]::Create($ip)
    

    这将是您的解决方法(如您所见):

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

    【讨论】:

    • 也许我遗漏了一些东西,Keith,但这基本上就是我在问题中报告的内容,不是吗?我只看到了一个外观差异,因为您在 $Block 参数上完全省略了一个类型,而我将它作为一个字符串......不过,我确实很欣赏 Connect 参考。
    • 是的,但是为了供其他人将来参考,最好在其中一个答案中解决问题。 :-)
    【解决方案2】:

    我意识到这并不能完全回答您的问题,但我认为您可以通过将其放入一个函数中来简化很多:

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

    由于Invoke-Command 已经可以为您启动工作,您可以让您的函数接受-Async 开关,然后将其值传递给-AsJob 开关。

    调用同步

    Start-Test -Block { ps | select -first 9 }
    

    异步调用

    Start-Test -Block { ps | select -first 9 } -Async
    

    推测

    至于您所看到的实际情况发生的原因,我不确定,但我认为这可能与嵌套脚本块有关,尽管我目前无法进行适当的测试。

    【讨论】:

    • 好建议,@briantist!不幸的是,这仅在我的特定情况下不起作用,因为我有多个需要作为作业运行的 Invoke-Command。
    【解决方案3】:

    我相信你看到这个的原因是 $using 的目的是在远程系统上使用之前扩展脚本块内局部变量的 - 它实际上并没有在远程会话中创建这些变量。脚本块与值最接近的是它的命令文本。

    【讨论】:

      【解决方案4】:

      虽然知道(感谢@KeithHill)我所看到的是一个已知问题是有用的——对不起,我的意思是“设计上”——但我真正的问题没有得到回答(“最终,当 $Block 参数输入为 [ScriptBlock] 时,有没有办法让 Start-Test 工作?")

      昨晚我突然得到了答案:

      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
      }
      

      请注意,在 Start-TestAsync 中,我在内部允许序列化发生($using:Block),将 ScriptBlock 转换为字符串,然后立即将其重新转换(创建)为 ScriptBlock,然后可以安全地传递它开始测试作为真正的 ScriptBlock。 对我来说,这是对我问题中的解决方法的重大改进,因为现在这两个函数上的公共 API 都是正确的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-07-09
        • 1970-01-01
        • 2017-06-20
        • 1970-01-01
        • 1970-01-01
        • 2023-01-28
        • 1970-01-01
        相关资源
        最近更新 更多