如何在ScriptBlock中传递$ _($ PSItem)

时间:2018-10-24 17:50:02

标签: powershell foreach pipe powershell-sdk

我基本上是在使用运行空间来构建自己的并行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

4 个答案:

答案 0 :(得分:2)

关键是通过调用$_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]

答案 1 :(得分: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 :(得分:0)

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

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包装该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

答案 3 :(得分:0)

# 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/','') }