Как передать $_ ($PSItem) в ScriptBlock
Я в основном строю свою собственную функцию параллельного конвейера 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. Почему рабочий процесс PowerShell значительно медленнее, чем сценарий, не относящийся к рабочему процессу, для анализа файлов XML
5 ответов
Ключ должен определить $_
в качестве переменной, которую может видеть ваш блок скрипта, с помощью вызова 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]
Я думаю, что вы ищете (а я искал) поддержку блока сценария "delay-bind", поддерживаемого в PowerShell 5.1+. Документация Microsoft немного рассказывает о том, что требуется, но не предоставляет никаких примеров пользовательских сценариев (в настоящее время).
Суть в том, что PowerShell неявно определит, что ваша функция может принимать блок сценария привязки задержки, если он определяет явно типизированный параметр конвейера (либо
by Value
или
by 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
), если требуются дополнительные параметры.
Раздражает то, что нет способа явно указать, что следует использовать delay-bind, а ошибки, возникающие в результате неправильного структурирования вашей функции, могут быть неочевидными.
Вы можете использовать ScriptBlock.InvokeWithContext
Метод для передачи входного объекта как$_
($PSItem
) на ваш powershell
экземпляры. Также стоит отметить, что, увидев последнее изменение вашего вопроса, вам обязательно следует добавить Ast.GetScriptBlock()
к
scriptblock
аргумент, чтобы лишить его привязки к пространству выполнения , иначе вы столкнетесь с проблемами, либо сбоем сеанса, либо взаимоблокировками. Подробную информацию см. в выпуске 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 { $_ }
# 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/','') }
Может быть, это может помочь. Обычно я запускаю автоматически сгенерированные задания следующим образом:
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