Как передать $_ ($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
Другие вопросы по тегам