Трубные полные массивы-объекты вместо элементов массива по одному за раз?
Как вы отправляете вывод из одного CmdLet в следующий в конвейере как полный массив-объект вместо отдельных элементов в массиве по одному за раз?
Проблема - Общее описание
Как видно из справки about_pipelines (help pipeline
), powershell отправляет объекты по одному вниз по конвейеру¹. Таким образом, Get-Process -Name notepad | Stop-Process
отправляет один процесс во время работы по трубе.
Предположим, что у нас есть сторонний CmdLet (Do-SomeStuff), который нельзя каким-либо образом изменить или изменить. Do-SomeStuff выполняет разные действия, если ему передан массив строк или передан один строковый объект.
Do-SomeStuff является лишь примером, его можно заменить на ForEach-Object
, Select-Object
, Write-Host
(или любой другой CmdLet, принимающий вход конвейера)
Do-SomeStuff будет в этом примере обрабатывать отдельные элементы в массиве в то время.
$theArray = @("A", "B", "C")
$theArray | Do-SomeStuff
Если мы хотим отправить полный массив как один объект в Do-SomeStuff, вы можете попробовать что-то вроде этого
@($theArray) | Do-SomeStuff
Но это не приводит к ожидаемому результату, поскольку PowerShell "игнорирует" новый массив с одним элементом.
Итак, как вы "принудительно" $theArray
передаваться по каналу как единый массив-объект, а не элементы контента в то время?
Проблема - практический пример
Как показано ниже, вывод Write-Host
отличается, если передан массив или если он передал отдельные элементы в массиве по одному в тот момент.
PS C:\> $theArray = @("A", "B", "C")
PS C:\> Write-Host $theArray
A B C
PS C:\> $theArray | foreach{Write-Host $_}
A
B
C
PS C:\> @($theArray) | foreach{Write-Host $_}
A
B
C
Как сделать, чтобы $theArray | foreach{Write-Host $_}
выдал тот же результат, что и Write-Host $theArray
?
СНОСКИ
- Обработка трубопроводов в Powershell
Обычный массив строк
PS C:\> @("A", "B", "C").GetType().FullName
System.Object[]
Обычный массив строк, переданных в Foreach-Object
PS C:\> @("A", "B", "C") | foreach{$_.GetType().FullName}
System.String
System.String
System.String
Каждая строка в массиве обрабатывается по одному в тот момент, когда объект-объект CmdLet ForEach-Object.
Массив массивов, где "внутренние" массивы представляют собой массивы строк.
PS C:\> @(@("A", "B", "C"), @("D", "E", "F"), @("G", "H", "I")) | foreach{$_.GetType().FullName}
System.Object[]
System.Object[]
System.Object[]
Каждый массив в массиве обрабатывается по одному в тот момент, когда объект CmdLet ForEach-Object, и содержимое каждого подматрица от входа обрабатывается как один объект, даже если это массив.
Ответы
Ответ 1
Короткий ответ: использовать оператор унарного массива ,
:
,$theArray | foreach{Write-Host $_}
Длинный ответ: есть одна вещь, которую вы должны понимать о операторе @()
: он всегда интерпретирует свой контент как инструкцию, даже если контент - это просто выражение. Рассмотрим этот код:
$a='A','B','C'
[email protected]($a;)
[email protected]($b;)
Здесь я добавляю явный конец выражения ;
, хотя PowerShell позволяет его опустить. $a
- это массив из трех элементов. Каков результат инструкции $a;
? $a
- это коллекция, поэтому сбор должен быть перечислим, и каждый отдельный элемент должен быть передан по конвейеру. Таким образом, результат оператора $a;
- это три элемента, записанных в конвейер. @($a;)
увидеть три элемента, но не исходный массив, и создать из них массив, поэтому $b
- это массив из трех элементов. Точно так же $c
- массив из трех элементов. Поэтому, когда вы пишете @($collection)
, вы создаете массив, который копирует элементы $collection
, а не массив одного элемента.
Ответ 2
Символ запятой делает данные массивом. Чтобы линия трубопровода обрабатывала ваш массив как массив, вместо того, чтобы работать с каждым элементом массива отдельно, вам также может понадобиться обернуть данные скобками.
Это полезно, если вам нужно оценить состояние нескольких элементов в массиве.
Используя следующую функцию
function funTest {
param (
[parameter(Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[alias ("Target")]
[array]$Targets
) # end param
begin {}
process {
$RandomSeed = $( 1..1000 | Get-Random )
foreach ($Target in $Targets) {
Write-Host "$RandomSeed - $Target"
} # next target
} # end process
end {}
} # end function
Рассмотрим следующие примеры:
Просто обкатка вашего массива в круглых скобках не гарантирует, что функция будет обрабатывать массив значений в одном вызове процесса. В этом примере мы видим изменение случайных чисел для каждого элемента в массиве.
PS C:\> @(1,2,3,4,5) | funTest
153 - 1
87 - 2
96 - 3
96 - 4
986 - 5
Простое добавление ведущей запятой также не гарантирует, что функция будет обрабатывать массив значений в одном вызове процесса. В этом примере мы видим изменение случайных чисел для каждого элемента в массиве.
PS C:\> , 1,2,3,4,5 | funTest
1000 - 1
84 - 2
813 - 3
156 - 4
928 - 5
С ведущей запятой и массивом значений в круглых скобках мы можем видеть, что случайное число остается неизменным, поскольку используется команда foreach.
PS C:\> , @( 1,2,3,4,5) | funTest
883 - 1
883 - 2
883 - 3
883 - 4
883 - 5
Ответ 3
Там есть решение для старой школы, если вы не возражаете, что ваш процесс является функцией.
Пример. Вы хотите, чтобы массив скопировался в буфер обмена таким образом, чтобы вы могли его снова создать на другой системе без каких-либо подключений PSRemoting. Поэтому вам нужен массив, содержащий "A", "B" и "C" для преобразования в строку:
@( "А", "В", "С" )
... вместо литерального массива.
Итак, вы строите это (что не является оптимальным по другим причинам, но оставайтесь на теме):
# Serialize-List
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$list
)
$output = "@(";
foreach ($element in $list)
{
$output += "`"$element`","
}
$output = $output.Substring(0, $output.Length - 1)
$output += ")"
$output
и он работает, когда вы указываете массив как параметр напрямую:
Сериализованный список $list
@( "А", "В", "С" )
... но не так много, когда вы передаете его по конвейеру:
$list | Сериализация-List
@( "С" )
Но реорганизуйте свою функцию с помощью начальных, технологических и конечных блоков:
# Serialize-List
param
(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$list
)
begin
{
$output = "@(";
}
process
{
foreach ($element in $list)
{
$output += "`"$element`","
}
}
end
{
$output = $output.Substring(0, $output.Length - 1)
$output += ")"
$output
}
... и вы получите желаемый результат в обоих направлениях.
Сериализованный список $list
@( "А", "В", "С" )
$list | Сериализация-List
@( "А", "В", "С" )