将完整的数组对象作为一次性管道传递,而不是逐个传递数组项?

40

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

问题 - 通用描述
正如在about_pipelines(帮助管道)中所示,PowerShell会逐个向下传送对象。因此,Get-Process -Name notepad | Stop-Process会将一个进程逐个发送到管道。

假设我们有一个第三方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“忽略”新的单项数组,它并没有产生预期的结果。

那么,如何“强制”$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命令按顺序逐个处理数组中的每个字符串。


一个数组,其中包含多个数组,每个“内部”数组都是由字符串组成的数组。

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

ForEach-Object CmdLet逐个处理数组中的每个数组,即使它是一个数组,也将输入的每个子数组的内容处理为一个对象。


3
PowerShell可以展开数组。关于这个话题,在这个网站和其他地方有讨论。展开的过程可能有点奇怪,但它是指你不能使用@(@(@()))获取多个数组(粗略地说)。 - Etan Reisner
10
请提一个问题。删除所有其他无关的内容(这些内容与您最初的问题无关),您的原始问题是您解释得最少的那个。在PowerShell中,将输出用@()括起来不起作用吗? - Etan Reisner
2
我同意@EtanReisner的观点,这很难理解。如果你有多个问题,请分别发布它们。即使一些支持示例重叠,也最好采用这种方式。正如提到的那样,PowerShell展开数组。它还尝试强制执行比较的右侧,这就是为什么交换对象可能会给出不同结果的原因。 - briantist
你想做什么?"A", "B", "C" | Format-Custom -Expand CoreOnly 证明你正在将整个数组推送到 Format-Custom 命令中,那么你想要什么呢? - Vincent De Smet
澄清、阐述并具体化了问题。 - NoOneSpecial
6个回答

48

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

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

长答案:有一件事情你应该明白关于@()运算符:它总是将其内容解释为语句,即使该内容只是一个表达式。考虑以下代码:

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

3
谢谢,现在你告诉我该找什么,这些都在文档中了。"_, 逗号运算符 作为二元运算符,逗号创建一个数组。作为一元运算符,逗号创建一个只有一个成员的数组。将逗号放在成员前面。$myArray = 1,2,3 $SingleArray = ,1" - NoOneSpecial
1
@NoOneSpecial,我更新了我的答案,并详细说明了@()运算符的行为。 - user4003407
那么C#中的等价语句是什么呢?object[] myArray = new object[] { new object[0] }; - NoOneSpecial
2
$array=,@() - user4003407
@PetSerAl 兄弟!!!你关于 @() 运算符总是将其内容解释为语句的评论真是太有启发性了!!!我在 PowerShell 中往往会犯错误,但这让许多代码片段突然变得清晰明了。 - Chiramisu
显示剩余4条评论

10

逗号字符将数据变成一个数组。为了让管道流程将您的数组视为数组,而不是单独处理每个数组元素,您还可能需要用括号包装数据。

如果需要评估数组中多个项的状态,则此方法非常有用。

使用以下函数:

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

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")

但是使用begin、process和end块重构您的函数:

# 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")

2

从函数使用实现

Write-Output -NoEnumerate

Write-Output 1, 2.2, '3' -NoEnumerate | Get-Member -Name GetType

从函数定义实现

将进程块的代码放在结束块中。

function PipelineDemoA {
    param (
        [Parameter(ValueFromPipeline)]
        [String[]]$Value = '.'
    )
    begin {
        Write-Output '----------begin'
        # $valueList = @() # Object[] cannot add objects dynamically
        $valueList = [System.Collections.ArrayList]@()
    }
    process {
        Write-Output 'process'
        $valueList.Add($Value) | Out-Null
    }
    end {
        Write-Output 'end'
        $Value = $PSBoundParameters['Value'] = $valueList
        Write-Output ($Value -join ', ')
    }
}

'A', 'B' |  PipelineDemoA
@() | PipelineDemoA
PipelineDemoA

使用自动变量$input
function PipelineDemoB {
    param (
        [Parameter(ValueFromPipeline)]
        [String[]]$Value
    )
    if ($input.Count -gt 0) { $Value = $PSBoundParameters['Value'] = $input }
}

'A', 'B', 'C' | PipelineDemoB

这种方法存在一个问题,即无法区分两种调用方法。不建议用于具有默认值的参数。

@() | PipelineDemoB
PipelineDemoB

# What will happen?
@() | Get-ChildItem # -Path is @()
Get-ChildItem # -Path is default value '.'

关于$input

在没有param块的函数中,$input变量是ArrayListEnumeratorSimple

在带有param块的函数中,在begin块中,$input变量是ArrayList[0]

在带有param块的函数中,在process块中,$input变量是ArrayList[1]

在带有param块的函数中,在end块中,$input变量是Object[0]

在带有param块但没有beginprocessend块的函数中,$input变量是Object[]

function PipelineDemo1 {
    begin {
        Write-Output '----------begin'
        Write-Output "$($input.GetType().Name) / $($input.MoveNext()) / $($input.Current)"
    }
    process {
        Write-Output '----------process'
        Write-Output "$($input.GetType().Name) / $($input.MoveNext()) / $($input.Current)"
    }
    end {
        Write-Output '----------end'
        Write-Output "$($input.GetType().Name) / $($input.MoveNext()) / $($input.Current)"
    }
}

'A', 'B', 'C' | PipelineDemo1 -Z 'Z'

function PipelineDemo2 {
    param (
        [Parameter(ValueFromPipeline)]
        [String[]]$Value,
        [string]$Z
    )
    begin {
        Write-Output '----------begin'
        Write-Output "$($input.GetType().Name) / $($input.Count) / $($input -join ', ')"
    }
    process {
        Write-Output '----------process'
        Write-Output "$($input.GetType().Name) / $($input.Count) / $($input -join ', ')"
    }
    end {
        Write-Output '----------end'
        Write-Output "$($input.GetType().Name) / $($input.Count) / $($input -join ', ')" # $input = Object[0]
    }
}

'A', 'B', 'C' | PipelineDemo2 -Z 'Z'

function PipelineDemo3 {
    param (
        [Parameter(ValueFromPipeline)]
        [String[]]$Value,
        [string]$Z
    )
    Write-Output '----------default'
    Write-Output "$($input.GetType().Name) / $($input.Count) / $($input -join ', ')"
}

'A', 'B', 'C' | PipelineDemo3 -Z 'Z'

function PipelineDemoValue {
    param (
        [string]$Tag,
        [Parameter(ValueFromPipeline)]
        [String[]]$Value = '.'
    )
    
    Write-Output "----------$Tag"
    Write-Output "Value = $($Value -join ', ') / PSValue = $($PSBoundParameters.ContainsKey('Value')) / $($PSBoundParameters['Value'] -join ', ')"
    Write-Output "input = $($input.Count) / $($input -join ', ')"
}

'A', 'B', 'C' | PipelineDemoValue -Tag 1
@() | PipelineDemoValue -Tag 2
$null | PipelineDemoValue -Tag 3
PipelineDemoValue -Value 'A', 'B', 'C' -Tag 4
PipelineDemoValue -Value $null -Tag 5
PipelineDemoValue -Tag 6

2

最“正确”的方法是使用Write-Output命令并指定-NoEnumerate开关:

Write-Output $theArray -NoEnumerate | Do-SomeStuff

此外,作者表示:

我有一种更像是hack的方法(我尽量避免使用hack)。您可以在将数组传输之前在其前面放置逗号。

两者都可以工作,但使用逗号运算符将始终创建一个附加数组来包含原始数组,而Write-Output -NoEnumerate将以一步将原始数组写入管道。


1
请详细说明为什么使用Write-Output比使用一元数组运算符更正确。 - NoOneSpecial
1
@NoOneSpecial,请看我的编辑。 - marsze
一步到位。Write-Output 需要接收数组,然后使用 WriteObject(arr, false) 不枚举输出:& {[CmdletBinding()]param($a) $PSCmdlet.WriteObject($a, $false) } (0..10) | % { "[$_]" }& {param($a) ($arr = [array]::CreateInstance([Object], 1))[0] = $a; $arr } (0..10) | % { "[$_]" } 没有任何区别。只需要看源代码... https://github.com/PowerShell/PowerShell/blob/0d6b93a23f92d3f39cd92eeaa7934d6d6778f089/src/System.Management.Automation/engine/DefaultCommandRuntime.cs#L60 - Santiago Squarzon
@SantiagoSquarzon,这两者是有区别的,请看 Write-Output @(1, 2, 3) | % { $_.GetType() }Write-Output @(1, 2, 3) -NoEnumerate | % { $_.GetType() } - marsze
源代码告诉我们逗号运算符和 cmdlet 所做的事情非常相似,除了 cmdlet 将集合添加到 List<T> 并将其发送到管道中。因此,在幕后,它们几乎是做同样的事情。我不是在争论“从 pwsh 视角来看使用什么更正确”,我只是说你在你的回答中所陈述的不准确。 - Santiago Squarzon
显示剩余8条评论

0
$ar="1","2","3"

$ar | foreach { $_ }

3
尽管这段代码片段可能解决了问题,但它并没有解释为什么或者如何回答这个问题。请在你的代码中包含一个解释,因为这真的有助于提高你的文章质量。记住,你正在为将来的读者回答问题,而那些人可能不知道你的代码建议的原因。你可以使用[编辑]按钮改进此答案以获得更多投票和声望! - Brian Tompsett - 汤莱恩
1
这种方法如何解决问题?$ar | foreach { $_ }$ar基本相同。它不像Write-Host $ar那样将数组作为一个对象处理。 - NoOneSpecial

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接