Вызов обратных вызовов из функций PowerShell

Я пытаюсь добавить некоторые функции обратного вызова к функции PowerShell, но я хочу иметь возможность определять поведение обратных вызовов вне определения функции, поэтому в основном очень простая инверсия Control для PowerShell.

Я стараюсь быть настолько же совместимым, насколько это практически возможно, и это то, к чему я придумал (обработка ошибок устранена для краткости):

$ErrorActionPreference = "Stop";
Set-StrictMode -Version "Latest";

function Invoke-DoSomethingMethod
{
 param
 (
 [hashtable] $Callbacks
 )
 $CallBacks["OnStarted"].Invoke();
 for( $i = 1; $i -le 10; $i++ )
 {
 # ... do something here ...
 $CallBacks["OnProgress"].Invoke($i * 10);
 }
 $CallBacks["OnFinished"].Invoke();
}

$myHostCallbacks = @{
 "OnStarted" = { write-host "started" }
 "OnProgress" = { write-host "processing $($args[0])%" }
 "OnFinished" = { write-host "finished" }
}

$myFileCallbacks = @{
 "OnStarted" = { Add-Content "C:\temp\log.txt" "started" }
 "OnProgress" = { Add-Content "C:\temp\log.txt" "processing $($args[0])%" }
 "OnFinished" = { Add-Content "C:\temp\log.txt" "finished" }
}

$myWebCallbacks = @{
 "OnStarted" = { Invoke-WebRequest -Uri "http://example.org/OnStarted" }
 "OnProgress" = { Invoke-WebRequest -Uri "http://example.org/OnProgress/$($args[0])" }
 "OnFinished" = { Invoke-WebRequest -Uri "http://example.org/OnFinish" }
}

Invoke-DoSomethingMethod -Callbacks $myHostCallbacks;

При запуске выводится следующее:

c:\temp>powershell .\callbacks.ps1

started
processing 10%
processing 20%
processing 30%
processing 40%
processing 50%
processing 60%
processing 70%
processing 80%
processing 90%
processing 100%
finished

Без использования внешних модулей или кода С# существует ли более идомальный способ сделать это с помощью PowerShell, который не полагается на сопоставление магических строк для имен обратного вызова и порядка параметров $ args []? (Предпочитаемая совместимость с версией 2.0).

Ура,

Майк

Обновить

В некотором контексте моя функция Invoke-DoSomething является частью структуры построения CI (https://github.com/ASOS/OctopusStepTemplateCi), которая в настоящее время жестко закодирована для отправки служебных сообщений TeamCity с использованием Write-Host во время процесса сборки.

Мы заинтересованы в поддержке других продуктов сервера сборки, поэтому я изучаю способ абстрагирования различных взаимодействий с сервером сборки в определяемые пользователем обратные вызовы, такие как "OnBuildStarted", "OnBuildStepFailed" и т.д., Как вы можете делать с IoC в С#.

Это упростит отправку поддержки для новых серверов сборки из проекта, а также для пользователей, которые будут писать обратные вызовы для серверов сборки, которые не выходят из коробки.

Это может помочь дать мне то, что я пытаюсь сделать:

function Invoke-BuildScript
{
 param( [hashtable] $Callbacks )
 $CallBacks["OnBuildStarted"].Invoke();
 ... build script here ...
 $CallBacks["OnBuildFinished"].Invoke();
}

$teamCityCallbacks = @{
 "OnBuildStarted" = { write-host "started" }
 "OnBuildFinished" = { write-host "finished" }
 "OnBuildError" = { write-host "error '$($args[0])'" }
}

$jenkinsCallbacks = @{
 "OnBuildStarted" = { ... }
 "OnBuildFinished" = { ... }
 "OnBuildError" = { ... }
}

Invoke-BuildScript -Callbacks $teamCityCallbacks;
3 ответа

Не так много ответов на мой оригинальный вопрос о IoC в PowerShell, но мне удалось как минимум скрыть магические строки и параметры обратного вызова за некоторыми командлетами оболочки, поэтому структура обратного вызова становится непрозрачным типом:

# knowledge of magic strings and parameter orders confined to here

function Invoke-OnStartedCallback
{
 param
 (
 [hashtable] $Callbacks
 )
 Invoke-Callback -Callbacks $CallBacks -Name "OnStarted";
}

function Invoke-OnProgressCallback
{
 param
 (
 [hashtable] $Callbacks,
 [int] $PercentComplete
 )
 Invoke-Callback -Callbacks $CallBacks -Name "OnProgress" -Parameters @( $PercentComplete );
}

function Invoke-OnFinishedCallback
{
 param
 (
 [hashtable] $Callbacks
 )
 Invoke-Callback -Callbacks $CallBacks -Name "OnFinished";
}

function New-DoSomethingCallbacks
{
 param
 (
 [scriptblock] $OnStarted,
 [scriptblock] $OnProgress,
 [scriptblock] $OnFinished
 )
 return @{
 "OnStarted" = $OnStarted
 "OnProgress" = $OnProgress
 "OnFinished" = $OnFinished
 };
}

Остальную часть кода можно записать следующим образом:

# no knowledge of magic strings below here

function Invoke-DoSomethingMethod
{
 param
 (
 [hashtable] $Callbacks
 )
 Invoke-OnStartedCallback $CallBacks;
 for( $i = 1; $i -le 10; $i++ )
 {
 # ... do something ...
 Invoke-OnProgressCallback $CallBacks -PercentComplete ($i * 10);
 }
 Invoke-OnFinishedCallback $CallBacks;
}


$myHostCallbacks = New-DoSomethingCallbacks -OnStarted { write-host "started" } '
 -OnProgress { write-host "processing $($args[0])%" } '
 -OnFinished { write-host "finished" };

$myFileCallbacks = New-DoSomethingCallbacks -OnStarted { Add-Content "C:\temp\log.txt" "started" } '
 -OnProgress { Add-Content "C:\temp\log.txt" "processing $($args[0])%" } '
 -OnFinished { Add-Content "C:\temp\log.txt" "finished" };

$myWebCallbacks = New-DoSomethingCallbacks -OnStarted { Invoke-WebRequest -Uri "http://example.org/OnStarted" } '
 -OnProgress { Invoke-WebRequest -Uri "http://example.org/OnProgress/$($args[0])" } '
 -OnFinished { Invoke-WebRequest -Uri "http://example.org/OnFinish" };

Invoke-DoSomethingMethod -Callbacks $myHostCallbacks;

с небольшим вспомогательным командлетом:

function Invoke-Callback
{
 param
 (
 [hashtable] $Callbacks,
 [string] $Name,
 [object[]] $Parameters
 )
 if( -not $Callbacks.ContainsKey($Name) )
 {
 return;
 }
 $callback = $Callbacks[$Name];
 if( $null -eq $callback )
 {
 return;
 }
 if( $null -eq $Parameters )
 {
 $callback.Invoke();
 }
 else
 {
 $callback.Invoke($Parameters);
 }
}


Я не совсем понимаю, что вы хотите сделать, но OnStarted, OnProgress, OnFinished kinda-sorta, похоже, OnFinished с концепциями конвейера PowerShell для Begin, Process, End.

Если у вас многокомпонентный конвейер, все они могут реализовать один или несколько из этих блоков.

В одном конвейере сначала выполняется выполнение блока Begin каждого элемента, затем каждый элемент Process block выполняется один раз для каждого элемента в конвейере (более поздние элементы могут быть пропущены, если предыдущий элемент не отправляет какой-либо вывод в конвейер) и, наконец, выполняется выполнение блока End каждого элемента.

Самый простой способ увидеть это без написания вашей собственной функции - это ForEach-Object который, хотя чаще всего используется только с блоком Process, также может запускать блоки Begin и End.

Демонстрация (с помощью одной пользовательской функции):

function Invoke-MyPipeline {
[CmdletBinding()]
param(
 [Parameter(ValueFromPipeline=$true)]
 $Value
)

 Begin {
 Write-Verbose -Message 'Begin Invoke-MyPipeline' -Verbose
 }

 Process {
 $Value * 2
 }

 End {
 Write-Verbose -Message 'End Invoke-MyPipeline' -Verbose
 }
}

1..10 | 
 Invoke-MyPipeline | 
 ForEach-Object -Begin { 
 Write-Verbose -Message 'ForEach (1) Begin' -Verbose
 } -Process {
 "~$_~"
 } -End {
 Write-Verbose -Message 'ForEach (1) End' -Verbose
 } |
 ForEach-Object -Begin { 
 Write-Verbose -Message 'ForEach (2) Begin' -Verbose
 } -Process {
 "@$_@"
 Start-Sleep -Milliseconds 500 # watch this effect on the whole pipeline
 } -End {
 Write-Verbose -Message 'ForEach (2) End' -Verbose
 }

Попробуйте прямо сейчас! (но вставьте его в ISE, чтобы увидеть эффект отсрочки синхронизации)

Так что это с вами связано?

Вместо того, чтобы называть блоки скриптов, вы можете просто взять их как массив. Тогда вы можете интерпретировать их таким же образом, что ForEach-Object делает (если есть 1 ScriptBlock, это Process, если есть 2, первый Begin, второй Process, если есть 3, то Begin, Process, End).

ForEach-Object также принимает дополнительные скриптовые блоки. Думаю, он управляет ими немного странно. Если 1 дополнительный, он (четвертый) будет рассматриваться как второй End блок. Если 2+ дополнительно, то дополнения N-1 рассматриваются как дополнительные блоки Process и End, а последний дополнительный обрабатывается как дополнительный End блок:

1,2,3|%{write-verbose 'A' -Verbose}{$_}{write-verbose 'B' -Verbose}{Write-Verbose 'Extra1' -Verbose}{Write-Verbose 'Extra2' -Verbose}{Write-Verbose 'Extra3' -Verbose}{Write-Verbose 'Extra4' -Verbose}

"Хорошо, но как это полезно?"

Потому что вместо повторного выполнения этих правил (или ваших собственных) вы можете просто позволить ForEach-Object сделать это за вас:

function Invoke-DoSomethingMethod {
[CmdletBinding()]
param (
 [Parameter(Mandatory = $true)]
 [ValidateNotNullOrEmpty()]
 [hashtable[]]
 $ScriptBlock # renaming to follow PowerShell conventions
)
 1..10 | 
 ForEach-Object -Process { $_*10 } | # this replaces your foreach
 ForEach-Object @ScriptBlock # Callbacks invoked here
}

$myHostCallbacks = @(
 { write-host "started" }
 ,{ write-host "processing $_%" } # bonus $_ usage
 ,{ write-host "finished" }
)

Invoke-DoSomethingMethod -ScriptBlock $myHostCallbacks

бонус

Вы можете использовать $_, который является общим и хорошо понятным шаблоном в PowerShell, не делая ничего лишнего.

Заметка

Вы все равно можете принять его как хэш-таблицу, и тот же код будет работать, если ключи хеш-таблицы соответствуют параметрам ForEach-Object, которые могут быть менее гибкими в этом случае.


Не уверен, что следующее, что вы ищете. Возможно, я тоже неправильно понял этот вопрос.

function Invoke-DoSomethingMethod ($OnStarted, $OnProgress, $OnFinished) {
 Invoke-Command -ScriptBlock $OnStarted
 for ( $i = 1; $i -le 10; $i++ ) {
 Invoke-Command -ScriptBlock $OnProgress
 }
 Invoke-Command -ScriptBlock $OnFinished
}

$OnStartedSb = [ScriptBlock]::Create('write-host "started"')
$OnProgressSb = [ScriptBlock]::Create('write-host "processing $($i * 10)%"')
$OnFinishedSb = [ScriptBlock]::Create('write-host "finished"')
Invoke-DoSomethingMethod -OnStarted $OnStartedSb -OnProgress $OnProgressSb -OnFinished $OnFinishedSb

licensed under cc by-sa 3.0 with attribution.