一次通过管道传输完整的数组对象而不是数组项?

Pipe complete array-objects instead of array items one at a time?

如何将一个 CmdLet 的输出作为完整的数组对象发送到管道中的下一个 CmdLet,而不是一次一个地发送数组中的单个项目?

问题 - 一般描述
正如在 about_pipelines (help pipeline) 的帮助中所见,powershell 一次向管道发送一个对象¹。因此 Get-Process -Name notepad | Stop-Process 一次向管道发送一个进程。

假设我们有一个无法以任何方式修改或更改的第 3 方 CmdLet (Do-SomeStuff)。如果传递给一个字符串数组或传递给一个字符串对象,Do-SomeStuff 的表现会有所不同。

Do-SomeStuff 只是一个示例,它可以替代 ForEach-ObjectSelect-ObjectWrite-Host(或任何其他接受管道输入的 CmdLet)

在此示例中,Do-SomeStuff 将一次处理数组中的单个项目。

$theArray = @("A", "B", "C")
$theArray | Do-SomeStuff

如果我们想将整个数组作为一个对象发送给 Do-SomeStuff,可以尝试这样的事情

@($theArray) | Do-SomeStuff

但它不会产生预期的结果,因为 PowerShell "ignores" 新的单项数组。

那么,如何 "force" $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 相同的输出?




脚注

  1. 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

ForEach-Object CmdLet 一次处理数组中的每个字符串。


数组数组,其中 "inner" 数组是字符串数组。

PS C:\> @(@("A", "B", "C"), @("D", "E", "F"), @("G", "H", "I")) | foreach{$_.GetType().FullName}
System.Object[]
System.Object[]
System.Object[]

数组中的每个数组由 ForEach-Object CmdLet 一次处理一个,来自输入的每个子数组的内容被作为一个对象处理,即使它是一个数组。

简答:使用一元数组运算符,

,$theArray | foreach{Write-Host $_}

长答案: 关于 @() 运算符,您应该了解一件事:它始终将其内容解释为语句,即使内容只是一个表达式。考虑这段代码:

$a='A','B','C'
$b=@($a;)
$c=@($b;)

我在这里添加了明确的语句结束标记 ;,尽管 PowerShell 允许省略它。 $a 是三个元素的数组。 $a; 语句的结果是什么? $a 是一个集合,所以应该枚举集合,每个单独的项目应该通过管道传递。所以 $a; 语句的结果是写入管道的三个元素。 @($a;) 看到三个元素,但不是原始数组,并从它们创建数组,所以 $b 是三个元素的数组。同样 $c 是相同的三个元素的数组。因此,当您编写 @($collection) 时,您会创建数组,即复制 $collection 的元素,而不是单个元素的数组。

逗号字符使数据成为数组。为了使管道将您的数组作为数组处理,而不是单独对每个数组元素进行操作,您可能还需要用括号将数据包裹起来。

如果您需要评估数组中多个项目的状态,这将非常有用。

使用下面的函数

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

如果您不介意您的进程是一个函数,那么有一个老式的解决方案。

示例:您希望将数组复制到剪贴板的方式允许您在没有任何 PSRemoting 连接的情况下在另一个系统上再次构建它。因此,您希望将包含“A”、“B”和“C”的数组转换为字符串: @("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

直接指定数组作为参数时有效:

Serialize-List $list
@("A","B","C")

...但是当你通过管道传递它时就没那么多了:

$list | Serialize-List
@("C")

但是使用开始、过程和结束块重构您的函数:

# 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
}

...您可以通过两种方式获得所需的输出。

Serialize-List $list
@("A","B","C")

$list | Serialize-List
@("A","B","C")
$ar="1","2","3"

$ar | foreach { $_ }

更多"correct" way would be to use the Write-Output cmdlet 并指定-NoEnumerate开关:

Write-Output $theArray -NoEnumerate | Do-SomeStuff

此外,作者指出:

I have a second way that's more of a hack (and I try to avoid hacks like this). You can place a comma in front of the array before you pipe it.

两者都可以,但使用 comma operator 将始终创建一个 附加 数组来包含原始数组,而 Write-Output -NoEnumerate 将写入 原始数组一步到管道