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

7
我基本上正在构建自己的并行foreach管道函数,使用runspaces。
我的问题是:我这样调用我的函数:
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()

编辑2:

问题是,例如内置函数ForEach-Object的实现如何实现在其ScriptBlock参数中不需要声明$_作为参数,并且我能否也使用这个功能?

(如果答案是,ForEach-Object是一个内置函数并且使用了一些我无法使用的魔法,那么在我看来,PowerShell语言整体上就不合格了)

编辑3:

感谢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,为什么PowerShell工作流在XML文件分析方面比非工作流脚本慢得多


要么检查脚本块的AST并注入参数声明(如果不存在),要么扩展PSCmdlet并使用dollarUnderscore参数集调用脚本块。 - Mathias R. Jessen
1
传递给您的脚本块的第一个参数在 $args[0] 中,或者如果它被视为管道,则在 $input 中。 - Maximilian Burszley
@MathiasR.Jessen:你能更具体一些吗?ForEach-Object / Where-Object等是否也是这样做的? - fjf2002
1
@mklement0:谢谢,我已经添加了Dispose调用,一个合理的ErrorActionPreference,并且我已经删除了“barrier”-现在完成的结果在所有runspace完成之前就会被传递到管道中。 - fjf2002
5个回答

8
关键是通过调用 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
我认为你正在寻找的(也是我在寻找的)是支持 PowerShell 5.1+ 中的 "delay-bind" script block。文档中对所需内容有一些介绍,但目前还没有提供任何用户脚本示例。
艰难的技术选择:手动实现
文档的要点是,如果你的函数定义了一个显式类型的管道参数(无论是按值还是按属性名),并且不是类型为 [scriptblock][object] 的参数,PowerShell 将会“隐式”检测到该函数可以接受延迟绑定的脚本块。
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
         [AllowEmptyString()]
         [Parameter(ValueFromPipeline)][string[]]$String,
         # this is our scriptblock parameter
         [Parameter(Position=0)][scriptblock]$Filter
     )

     Process {
         foreach($s in $String) {
             if (&$filter $s) {
                 Write-Output $s
             }
         }
     }
 }


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

请注意,延迟绑定(delay-bind)受以下限制:
- 仅当输入通过管道传递到函数时,才会应用延迟绑定。 - 作用域和闭包的应用方式与内置的延迟绑定 cmdlet 不同。
令人沮丧的是,并没有明确指定使用延迟绑定的方法,因此由于函数结构不正确而导致的错误可能不容易发现。
更简单的替代方法:使用内置的 Cmdlet
PowerShell 提供了内置的 Cmdlet,用于实现迭代/转换(ForEach-Object)和过滤(Where-Object)的延迟绑定,这涵盖了大多数需要使用延迟绑定的情况。
使用这些内置的 Cmdlet 可以轻松构建自定义的延迟绑定函数,而不受上述列出的限制。
function Test-WhereBasedFilter {
    param(
        [Parameter(ValueFromPipeline)]
        [object[]]
        $Object,

        [Parameter(Mandatory,Position=0)]
        [scriptblock]
        $Filter
    )

    process {
        foreach ($o in $object) {
            $o | Where-Object $Filter | Write-Output
        }
    }
}

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


function Test-ForBasedIterator {
    param(
        [Parameter(ValueFromPipeline)]
        [object[]]
        $Object,

        [Parameter(Mandatory,Position=0)]
        [scriptblock]
        $ScriptBlock
    )

    process {
        foreach ($o in $object) {
            $o | ForEach-Object $ScriptBlock | Write-Output
        }
    }
}

# sample invocation
> 'foo', 'fi', 'foofoo', '', $null, 'fib' | Test-ForBasedIterator { " 
 $_  foo!" }
  foo  foo!
  fi  foo!
  foofoo  foo!
    foo!
  fib  foo!

构建一个自定义的延迟绑定,可以更快地添加功能:
  • 输入可以作为标准参数传递,也可以通过管道传递
  • 对象输入正常工作
  • 作用域和闭包根据您使用的内置函数进行处理(这更符合用户的期望)

这应该是现在被接受的答案了。对我来说有效。 - Sled
这应该是现在应该被接受的答案了。这对我起作用。 - undefined
@Sled - 我已经更新了答案,包括我在原帖之后发现的更简单/更好的选项,并进一步进行了澄清。同时,将管道参数转换为非强制性,以便接受null/empty值。 - Tydaeus

2
你可以使用ScriptBlock.InvokeWithContext方法将输入对象作为$_$PSItem)传递给你的powershell实例。值得注意的是,根据你问题的最后编辑,你应该绝对在scriptblock参数中添加Ast.GetScriptBlock()以去除其运行空间亲和性,否则你将遇到问题,可能会导致会话崩溃或死锁。详细信息请参见GitHub问题#4003
如果你正在寻找一个更高级的函数版本,请参考这个答案或者在GitHub仓库中找到一个更高级的版本,该版本不使用runspacepool
function MyNewForeachFunction {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline)]
        [psobject] $PipelineObject,

        [Parameter(Mandatory, Position = 0)]
        [scriptblock] $ScriptBlock
    )

    process {
        try {
            # `.Ast.GetScriptBlock()` Needed to avoid runspace affinity issues!
            $ps = [powershell]::Create().AddScript({
                param([scriptblock] $sb, [psobject] $inp)

                $sb.InvokeWithContext($null, [psvariable]::new('_', $inp))
            }).AddParameters(@{
                sb  = $ScriptBlock.Ast.GetScriptBlock()
                inp = $PipelineObject
            })
            
            # using `.Invoke()` for demo purposes, would use `.BeginInvoke()`
            # instead for multi-threading
            $ps.Invoke()

            if ($ps.HadErrors) {
                foreach ($e in $ps.Streams.Error) {
                    $PSCmdlet.WriteError($e)
                }
            }
        }
        finally {
            if ($ps) {
                $ps.Dispose()
            }
        }
    }
}

0..10 | MyNewForeachFunction { $_ }

1
也许这可以帮助你。 通常我会以这种方式并行运行自动生成的作业:
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()。您可以尝试使用另一个脚本块来包装它。以下是我的示例的解决方法:

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接