Как написать расширенную функцию 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 я считаю зарезервированным словом. Вы можете использовать его, но я думаю, что вам, возможно, придется настроить его в другом наборе параметров.

Другие вопросы по тегам