【问题标题】:How to pass $_ ($PSItem) in a ScriptBlock如何在 ScriptBlock 中传递 $_ ($PSItem)
【发布时间】:2019-03-29 05:37:36
【问题描述】:

我基本上是在使用运行空间构建自己的并行 foreach 管道函数。

我的问题是:我这样调用我的函数:

somePipeline | MyNewForeachFunction { scriptBlockHere } | pipelineGoesOn...

如何将$_ 参数正确传递到ScriptBlock?它在 ScriptBlock 包含作为第一行时起作用

param($_)

但您可能已经注意到,powershell 内置的 ForEach-Object 和 Where-Object 不需要在传递给它们的每个 ScriptBlock 中都需要这样的参数声明。

提前感谢您的回答 fjf2002

编辑:

目标是:我希望函数 MyNewForeachFunction 的用户感到舒适——他们不应该在他们的脚本块中写一行 param($_)

MyNewForeachFunction内部,目前调用的ScriptBlock是通过

$PSInstance = [powershell]::Create().AddScript($ScriptBlock).AddParameter('_', $_)
$PSInstance.BeginInvoke()

EDIT2:

关键是,例如内置函数ForEach-Object 的实现如何实现$_ 不需要在其ScriptBlock 参数中声明为参数,我也可以使用该功能吗?

(如果答案是,ForEach-Object 是一个内置函数,并且使用了一些我无法使用的魔法,那么在我看来,这将取消整个语言 PowerShell 的资格)

EDIT3:

感谢 mklement0,我终于可以构建我的通用 foreach 循环。代码如下:

function ForEachParallel {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory)] [ScriptBlock] $ScriptBlock,
        [Parameter(Mandatory=$false)] [int] $PoolSize = 20,
        [Parameter(ValueFromPipeline)] $PipelineObject
    )

    Begin {
        $RunspacePool = [runspacefactory]::CreateRunspacePool(1, $poolSize)
        $RunspacePool.Open()
        $Runspaces = @()
    }

    Process {
        $PSInstance = [powershell]::Create().
            AddCommand('Set-Variable').AddParameter('Name', '_').AddParameter('Value', $PipelineObject).
            AddCommand('Set-Variable').AddParameter('Name', 'ErrorActionPreference').AddParameter('Value', 'Stop').
            AddScript($ScriptBlock)

        $PSInstance.RunspacePool = $RunspacePool

        $Runspaces += New-Object PSObject -Property @{
            Instance = $PSInstance
            IAResult = $PSInstance.BeginInvoke()
            Argument = $PipelineObject
        }
    }

    End {
        while($True) {
            $completedRunspaces = @($Runspaces | where {$_.IAResult.IsCompleted})

            $completedRunspaces | foreach {
                Write-Output $_.Instance.EndInvoke($_.IAResult)
                $_.Instance.Dispose()
            }

            if($completedRunspaces.Count -eq $Runspaces.Count) {
                break
            }

            $Runspaces = @($Runspaces | where { $completedRunspaces -notcontains $_ })
            Start-Sleep -Milliseconds 250
        }

        $RunspacePool.Close()
        $RunspacePool.Dispose()
    }
}

部分代码来自 MathiasR.Jessen,Why PowerShell workflow is significantly slower than non-workflow script for XML file analysis

【问题讨论】:

  • 要么检查脚本块的 AST,如果不存在参数声明,则注入参数声明,或者扩展 PSCmdlet 并使用 DollarUnderscore 参数集调用脚本块
  • 传递给你的脚本块的第一个参数在$args[0] 或者如果它被当作管道:$input
  • @MathiasR.Jessen:你能说得更具体点吗? ForEach-Object / Where-Object 等是否也这样做?
  • @mklement0:谢谢,我已经添加了 Dispose 调用、一个理智的 ErrorActionPreference 并且我已经移除了“障碍” - 现在完成的结果会在所有运行空间之前传递到管道中已经结束了。

标签: powershell foreach pipe powershell-sdk


【解决方案1】:

我认为您正在寻找的(以及我一直在寻找的)是支持 PowerShell 5.1+ 中支持的“延迟绑定”脚本块。 Microsoft documentation 说明了一些要求,但不提供任何用户脚本示例(目前)。

要点是 PowerShell 将隐式检测到您的函数可以接受延迟绑定脚本块,前提是它定义了显式类型的管道参数(by Valueby PropertyName),只要因为它不是[scriptblock] 类型或[object] 类型。

function Test-DelayedBinding {
    param(
        # this is our typed pipeline parameter
        # per doc this cannot be of type [scriptblock] or [object],
        # but testing shows that type [object] may be permitted
        [Parameter(ValueFromPipeline, Mandatory)][string]$string,
        # this is our scriptblock parameter
        [Parameter(Position=0)][scriptblock]$filter
    )

    Process {
        if (&$filter $string) {
            Write-Output $string
        }
    }
}

# sample invocation
>'foo', 'fi', 'foofoo', 'fib' | Test-DelayedBinding { return $_ -match 'foo' }
foo
foofoo

请注意,延迟绑定仅在输入通过管道传输到函数时才会应用,并且如果需要其他参数,脚本块必须使用命名参数(而不是 $args)。

令人沮丧的部分是没有办法明确指定应该使用延迟绑定,并且由于函数结构不正确而导致的错误可能不明显。

【讨论】:

    【解决方案2】:
    # I was looking for an easy way to do this in a scripted function,
    # and the below worked for me in PSVersion 5.1.17134.590
    
    function Test-ScriptBlock {
        param(
            [string]$Value,
            [ScriptBlock]$FilterScript={$_}
        )
        $_ = $Value
        & $FilterScript
    }
    Test-ScriptBlock -Value 'unimportant/long/path/to/foo.bar' -FilterScript { [Regex]::Replace($_,'unimportant/','') }
    

    【讨论】:

      【解决方案3】:

      关键是将$_ 定义为一个变量,您的脚本块可以通过调用Set-Variable 看到该变量。

      这是一个简单的例子:

      function MyNewForeachFunction {
        [CmdletBinding()]
        param(
          [Parameter(Mandatory)]
          [scriptblock] $ScriptBlock
          ,
          [Parameter(ValueFromPipeline)]
          $InputObject
        )
      
        process {
          $PSInstance = [powershell]::Create()
      
          # Add a call to define $_ based on the current pipeline input object
          $null = $PSInstance.
            AddCommand('Set-Variable').
              AddParameter('Name', '_').
              AddParameter('Value', $InputObject).
            AddScript($ScriptBlock)
      
          $PSInstance.Invoke()
        }
      
      }
      
      # Invoke with sample values.
      1, (Get-Date) | MyNewForeachFunction { "[$_]" }
      

      上面的结果类似于:

      [1]
      [10/26/2018 00:17:37]
      

      【讨论】:

        【解决方案4】:

        也许这会有所帮助。 我通常会以这种方式并行运行自动生成的作业:

        Get-Job | Remove-Job
        
        foreach ($param in @(3,4,5)) {
        
         Start-Job  -ScriptBlock {param($lag); sleep $lag; Write-Output "slept for $lag seconds" } -ArgumentList @($param)
        
        }
        
        Get-Job | Wait-Job | Receive-Job
        

        如果我理解正确,您正试图摆脱脚本块中的 param()。您可以尝试用另一个包裹那个 SB。以下是我的示例的解决方法:

        Get-Job | Remove-Job
        
        #scriptblock with no parameter
        $job = { sleep $lag; Write-Output "slept for $lag seconds" }
        
        foreach ($param in @(3,4,5)) {
        
         Start-Job  -ScriptBlock {param($param, $job)
          $lag = $param
          $script = [string]$job
          Invoke-Command -ScriptBlock ([Scriptblock]::Create($script))
         } -ArgumentList @($param, $job)
        
        }
        
        Get-Job | Wait-Job | Receive-Job
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-11-07
          • 1970-01-01
          • 2023-01-28
          • 2013-07-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多