Как написать расширенную функцию PowerShell, которая может работать как с объектами в конвейере, так и с объектами, полученными из значения параметра?
Я пишу функцию Chunk-Object
который может разбивать массив объектов на подмассивы. Например, если я передам это массив @(1, 2, 3, 4, 5)
и указать 2
элементов на чанк, тогда он вернет 3 массива @(1, 2)
, @(3, 4)
а также @(5)
, Также пользователь может предоставить необязательный scriptblock
параметр, если они хотят обработать каждый элемент, прежде чем разбить их на подмассивы. Теперь мой код:
function Chunk-Object()
{
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true)] [object[]] $InputObject,
[Parameter()] [scriptblock] $Process,
[Parameter()] [int] $ElementsPerChunk
)
Begin {
$cache = @();
$index = 0;
}
Process {
foreach($o in $InputObject) {
$current_element = $o;
if($Process) {
$current_element = & $Process $current_element;
}
if($cache.Length -eq $ElementsPerChunk) {
,$cache;
$cache = @($current_element);
$index = 1;
}
else {
$cache += $current_element;
$index++;
}
}
}
End {
if($cache) {
,$cache;
}
}
}
(Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$_ + 100} -ElementsPerChunk 3)
Write-Host "------------------------------------------------"
(echo 1 2 3 4 5 6 7 | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3)
Результат:
PS C:\Users\a> C:\Untitled5.ps1
100
100
100
100
100
100
100
------------------------------------------------
101
102
103
104
105
106
107
PS C:\Users\a>
Как видите, он работает с объектами, переданными по конвейеру, но не работает со значениями, полученными из параметра. Как изменить код, чтобы он работал в обоих случаях?
4 ответа
Разница в том, что при передаче массива в Chunk-Object функция выполняет блок процесса один раз для каждого элемента массива, переданного в виде последовательности объектов конвейера, тогда как при передаче массива в качестве аргумента параметру -InputObject, блок процесса выполняется один раз для всего массива, который целиком присваивается $InputObject.
Итак, давайте посмотрим на ваш конвейерный вариант команды:
echo 1 2 3 4 5 6 7 | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Причина, по которой это работает, заключается в том, что для каждой итерации конвейера $_ устанавливается равным значению текущего элемента массива в конвейере, который также назначается переменной $InputObject (как одноэлементный массив, из-за [object[]]
напечатанный. Цикл foreach на самом деле лишен в этом случае, потому что массив $InputObject всегда имеет один элемент для каждого вызова блока процесса. Вы могли бы на самом деле удалить цикл и изменить $current_element = $o
в $current_element = $InputObject
и вы получите точно такие же результаты.
Теперь давайте рассмотрим версию, которая передает аргумент массива в -InputObject:
Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$_ + 100} -ElementsPerChunk 3
Причина, по которой это не работает, состоит в том, что блок скрипта, который вы передаете параметру -Process, содержит $_, но цикл foreach присваивает каждому элементу $ o, а $_ нигде не определен. Все элементы в результатах равны 100, потому что каждая итерация устанавливает $current_element в результаты блока сценария. {$_ + 100}
, который всегда оценивается в 100, когда $_ равен нулю. Чтобы доказать это, попробуйте изменить $_ в блоке скриптов на $ o, и вы получите ожидаемые результаты:
Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$o + 100} -ElementsPerChunk 3
Если вы хотите использовать $_ в блоке скриптов, измените цикл foreach на конвейер, просто заменив foreach($o in $InputObject) {
с $InputObject | %{
, Таким образом, обе версии будут работать, потому что функция Chunk-Object использует конвейер внутри, поэтому $_ устанавливается последовательно для каждого элемента массива, независимо от того, вызывается ли блок процесса несколько раз для серии отдельных элементов массива, переданных в в качестве конвейерного ввода или только один раз для многоэлементного массива.
ОБНОВИТЬ:
Я посмотрел на это снова и заметил, что в строке
$current_element = & $Process $current_element;
вы, похоже, пытаетесь передать $current_element в качестве аргумента блоку сценария в $ Process. Это не работает, потому что параметры, передаваемые в блок скриптов, работают в основном так же, как и в функциях. Если вы вызываете MyFunction 'foo'
тогда 'foo' автоматически не присваивается $_ внутри функции; то же самое, & {$_ + 100} 'foo'
не устанавливает $_ в 'foo'. Измените аргумент блока скрипта на {$args[0] + 100}
и вы получите ожидаемые результаты с передачей или без ввода в конвейер:
Chunk-Object -InputObject (echo 1 2 3 4 5 6 7) -Process {$args[0] + 100} -ElementsPerChunk 3
Обратите внимание, что хотя эта версия аргумента scriptblock работает, даже если вы продолжаете цикл foreach, я все равно рекомендую использовать Foreach-Object ($InputObject | %{
), потому что это обычно более эффективно, поэтому функция будет работать быстрее для больших объемов данных.
Технически проблема не в атрибутах параметров. Это как с вашими аргументами, так и с тем, как вы их обрабатываете.
Проблема: (echo 1 2 3 4 5 6 7)
создает строку со значением "1 2 3 4 5 6 7", вы хотите обработать массив
Решение: использовать массив: @(1, 2, 3, 4, 5, 6, 7)
Проблема: вы используете оператор foreach. Это делает пакетную обработку, а не конвейер
Решение: использовать foreach-объект
Process {
$InputObject | Foreach-Object {
...
}
}
foreach($foo in $bar)
соберет все предметы, затем итерирует. $list | Foreach-Object { ... }
обрабатывает каждый элемент отдельно, позволяя конвейеру продолжить
Примечание. Если входные данные на самом деле являются строкой, вам также придется разделить строку и преобразовать каждый элемент в целое число; Либо измените тип аргумента на целое, если это то, что вы ожидаете.
Окончательный ответ:
function Chunk-Object()
{
[CmdletBinding()]
Param(
[Parameter(Mandatory = $true,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true)] [object[]] $InputObject,
[Parameter()] [scriptblock] $Process,
[Parameter()] [int] $ElementsPerChunk
)
Begin {
$cache = @();
$index = 0;
}
Process {
$InputObject | ForEach-Object {
$current_element = $_;
if($Process) {
$current_element = & $Process $current_element;
}
if($cache.Length -eq $ElementsPerChunk) {
,$cache;
$cache = @($current_element);
$index = 1;
}
else {
$cache += $current_element;
$index++;
}
}
}
End {
if($cache) {
,$cache;
}
}
}
Set-PSDebug -Off
Write-Host "Input Object is array"
Chunk-Object -InputObject @(1, 2, 3, 4, 5, 6, 7) -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input Object is on pipeline"
@(1, 2, 3, 4, 5, 6, 7) | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is string"
(echo "1 2 3 4 5 6 7") | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is split string"
(echo "1 2 3 4 5 6 7") -split ' ' | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is int[] converted from split string"
([int[]]("1 2 3 4 5 6 7" -split ' ')) | Chunk-Object -Process {$_ + 100} -ElementsPerChunk 3
Write-Host "------------------------------------------------"
Write-Host "Input object is split and converted"
(echo "1 2 3 4 5 6 7") -split ' ' | Chunk-Object -Process {[int]$_ + 100} -ElementsPerChunk 3
PowerShell автоматически разворачивает объекты, которые передаются по каналу, поэтому возникает разница в поведении.
Рассмотрим следующий код:
function Test {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true)]
[Object[]] $InputObject
)
process {
$InputObject.Count;
}
}
# This example shows how the single array is passed
# in, containing 4 items.
Test -InputObject (1,2,3,4);
# Result: 4
# This example shows how PowerShell unwraps the
# array and treats each object individually.
1,2,3,4 | Test;
# Result: 1,1,1,1
Имея это в виду, мы должны обрабатывать ввод по-разному, в зависимости от того, как он передается.
function Test {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true)]
[Object[]] $InputObject
, [ScriptBlock] $Process
)
process {
if ($InputObject.Count -gt 1) {
foreach ($Object in $InputObject) {
Invoke-Command -ScriptBlock $Process -ArgumentList $Object;
}
}
else {
Invoke-Command -ScriptBlock $Process -ArgumentList $InputObject;
}
}
}
Test -InputObject (1,2,3,4) -Process { $args[0] + 100 };
Write-Host -Object '-----------------';
1,2,3,4 | Test -Process { $args[0] + 100; };
Если вы хотите, чтобы пользователь мог использовать $_
вместо $args[0]
, то вам нужно убедиться, что пользователь функции включает process { ... }
блок внутри их ScriptBlock. Смотрите следующий пример.
function Test {
[CmdletBinding()]
param (
[Parameter(ValueFromPipeline = $true)]
[Object[]] $InputObject
, [ScriptBlock] $Process
)
process {
if ($InputObject.Count -gt 1) {
foreach ($Object in $InputObject) {
$Object | & $Process;
}
}
else {
$_ | & $Process;
}
}
}
Test -InputObject (1,2,3,4) -Process { process { $_ + 100; }; };
Write-Host -Object '-----------------';
1,2,3,4 | Test -Process { process { $_ + 100; }; };
Вместо использования $Inputobject, попробуйте дать ему имя параметра, например, $ Input. Вот пример функции, которую я использую для обучения, которая объясняет, как:
Function Get-DriveC {
[cmdletbinding()]
Param(
[Parameter(ValueFromPipeline)]
[ValidateNotNullorEmpty()]
[string[]]$Computername = $env:computername)
Begin {
Write-Verbose "Starting Get-DriveC"
#define a hashtable of parameters to splat
$param=@{Computername=$null;class="win32_logicaldisk";errorAction="Stop";
filter="deviceid='c:'"}
}
Process {
foreach ($computer in $computername) {
Try {
Write-Verbose "Querying $computer"
$param.Computername=$computer
Get-CimInstance @param
}
Catch {
Write-Warning "Oops. $($_.exception.message)"
}
} #foreach
} #process
End {
Write-Verbose "Ending Get-DriveC"
}
} #end function
Я могу передать ему имена компьютеров или передать массив в качестве значения параметра.
InputObject я считаю зарезервированным словом. Вы можете использовать его, но я думаю, что вам, возможно, придется настроить его в другом наборе параметров.