39

Assume we have:

$a = @(1, @(2, @(3)))

I would like to flatten $a to get @(1, 2, 3).

I have found one solution:

@($a | % {$_}).count

But maybe there is a more elegant way?

1
  • 1
    For anyone less experienced and confused by this discussion, the flattening is done by @($a | % {$_}). The .count method reports 2 items for the original 3-dimensional array, and 3 items for the flattened array. Commented Jul 18, 2022 at 8:40

5 Answers 5

30

Piping is the correct way to flatten nested structures, so I'm not sure what would be more "elegant". Yes, the syntax is a bit line-noisy looking, but frankly quite serviceable.

2020 Edit

The recommended syntax these days is to expand % to ForEach-Object. A bit more verbose but definitely more readable:

@($a | ForEach-Object {$_}).count
Sign up to request clarification or add additional context in comments.

1 Comment

Piping to % works, but not piping to measure-object, or ?. It must be because of the way % processes input from a pipeline, rather than the piping itself.
14

Same code, just wrapped in function:

function Flatten($a)
{
    ,@($a | % {$_})
}

Testing:

function AssertLength($expectedLength, $arr)
{
    if($ExpectedLength -eq $arr.length) 
    {
        Write-Host "OK"
    }
    else 
    {
        Write-Host "FAILURE"
    }
}

# Tests
AssertLength 0 (Flatten @())
AssertLength 1 (Flatten 1)
AssertLength 1 (Flatten @(1))
AssertLength 2 (Flatten @(1, 2))
AssertLength 2 (Flatten @(1, @(2)))
AssertLength 3 (Flatten @(1, @(2, @(3))))

1 Comment

Prefixing with a comma forces powershell to return an array even if there is only one item to return. Without the comma Powershell will unravel the array into a singe object of the same type as the array contents. This can be very confusing to people coming from other strongly types languages.
5

Warning: See edit at the end!

This problem is probably most elegantly resolved with the .ForEach() array method introduced in Powershell v4.0. Performance-wise it has the advantage of not needing to construct a pipeline, so in some cases it might perform better.

> $a.ForEach({$_}).Count
3

If you already have a pipeline, the easiest way to flatten an array is to pipe it through Write-Output:

> $b = $a | Write-Output
> $b.Count
3

--

EDIT: The answer above is not really correct. It doesn't completely flatten arrays that have multiple nested arrays. The answer from @SantiagoSquarzon has an example of a deeply nested array that requires multiple unrolls:

> $toUnroll = @(@(0,1),@(2,3),@(@(4,@(5,6)),@(7,8),9),10) # 11 elements
> $toUnroll.ForEach({$_}).Count
8

> $toUnroll.ForEach({$_}).ForEach({$_}).Count
10

> $toUnroll.ForEach({$_}).ForEach({$_}).ForEach({$_}).Count
11

Or, perhaps more clearly:

> $toUnroll = @(@(0,1),@(2,3),@(@(4,@(5,6)),@(7,8),9),10) # 11 elements
### Unroll 0 times
> $toUnroll.ForEach({$_ | ConvertTo-Json -Compress})
[0,1]
[2,3]
[[4,[5,6]],[7,8],9]
10

### Unroll 1 times
> $toUnroll.ForEach({$_}).ForEach({$_ | ConvertTo-Json -Compress}) 
0
1
2
3
[4,[5,6]]
[7,8]
9
10

### Unroll 2 times
> $toUnroll.ForEach({$_}).ForEach({$_}).ForEach({$_ | ConvertTo-Json -Compress})
0
1
2
3
4
[5,6]
7
8
9
10

### Unroll 3 times
> $toUnroll.ForEach({$_}).ForEach({$_}).ForEach({$_}).ForEach({$_ | ConvertTo-Json -Compress})
0
1
2
3
4
5
6
7
8
9
10

Comments

5

There are examples of nested arrays where piping to ForEach-Object simply can't handle them.

For example, given our nested array:

$toUnroll = @(@(0,1),@(2,3),@(@(4,@(5,6)),@(7,8),9),10)

If we attempt to pipe to ForEach-Object, the result would be:

PS /> $toUnroll | ForEach-Object { $_ }

0
1
2
3
4

Length         : 2
LongLength     : 2
Rank           : 1
SyncRoot       : {5, 6}
IsReadOnly     : False
IsFixedSize    : True
IsSynchronized : False
Count          : 2

7
8
9
10

Write-Output is also not able to handle the unrolling:

$toUnroll | Write-Output | ForEach-Object GetType

IsPublic IsSerial Name             BaseType
-------- -------- ----             --------
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Object[]         System.Array
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType

Below we can see some examples on how we can handle the flattening of these nested arrays including a one-liner anonymous function.

This technique unrolls our array in order.

function RecursiveUnroll {
    [cmdletbinding()]
    param(
        [parameter(Mandatory, ValueFromPipeline)]
        [object[]] $Unroll
    )

    process {
        foreach($item in $Unroll) {
            if($item -is [object[]]) {
                RecursiveUnroll -Unroll $item
                continue
            }
            $item
        }
    }
}

RecursiveUnroll -Unroll $toUnroll
# Results in an array from 0 to 10
  • One-liner anonymous function:

The logic for this script block is the exact same as the function demonstrated above.

$toUnroll | & { process { if($_ -is [object[]]) { return $_ | & $MyInvocation.MyCommand.ScriptBlock }; $_ }}
  • Recursive Class Method (can be static or instance)

Same as the recursive function example, we can expect the array to keep it's order. We can add that this technique should be faster than the recursive function approach since repeated function or script block calls is expensive, this is hinted in PowerShell scripting performance considerations.

class Unroller {
    [object[]] $Array

    Unroller() { }
    Unroller([object[]] $Array) {
        $this.Array = $Array
    }

    static [object] Unroll([object[]] $Array) {
        $result = foreach($item in $Array) {
            if($item -is [object[]]) {
                [Unroller]::Unroll($item)
                continue
            }
            $item
        }
        return $result
    }

    [object] Unroll () {
        return [Unroller]::Unroll($this.Array)
    }
}

# Instantiating and using using the instance method of our class:
$instance = [Unroller] $toUnroll
$instance.Unroll()
# Results in an array from 0 to 10

# Using the static method of our class, no need to instantiate:
[Unroller]::Unroll($toUnroll)
# Results in an array from 0 to 10

This technique should be the fastest one, the downside is that we cannot expect an ordered array.

$queue = [System.Collections.Generic.Queue[object]]::new()
$queue.Enqueue($toUnroll)

while($queue.Count) {
    foreach($item in $queue.Dequeue()) {
        if($item -is [object[]]) {
            $queue.Enqueue($item)
            continue
        }
        $item
    }
}

# Using our given nested array as an example we can expect
# a flattened array with the following order:
# 10, 0, 1, 2, 3, 9, 4, 7, 8, 5, 6

Lastly, using a Stack we can ensure that the order is preserved, this technique is also very efficient.

$stack = [System.Collections.Generic.Stack[object]]::new()
$stack.Push($toUnroll)

$result = while($stack.Count) {
    foreach($item in $stack.Pop()) {
        if($item -is [object[]]) {
            [array]::Reverse($item)
            $stack.Push($item)
            continue
        }
        $item
    }
}

[array]::Reverse($result)
$result # Should be array from 0 to 10

1 Comment

Very interesting answer! I noticed that while $toUnroll | Write-Output didn't work, $toUnroll | Write-Output | Write-Output did. Of course, that just punts the problem further down the line. An even deeper array would likely cause the issue to recur.
1

You can use .NET's String.Join method.

[String]::Join("",$array)

2 Comments

This question is about concatenating arrays, not strings.
I came here with the intention to flattening an array of strings - and this did the job :) And btw. this is as I read the title "Flatten Array in PowerShell". +1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.