Thursday, October 9, 2014

PowerShell and arrays a deceptive combination


So I was reading up on PowerShell arrays on this TechNet site:
 
It gives a brief introduction to PowerShell arrays and illustrates how powerful they are. However, arrays in PowerShell are a curious beast indeed and in some cases they can deceive you, so I thought I'd give some additional insights into it in the hopes it may save you from struggling with them for too long.
 
Let's start with the basics. One thing you can do with arrays is concatenation:
$a = 1,2,3
$a += 4
 
Now $a is 1,2,3,4. Neat.
 
But wait a minute… let's try changing this example a bit
$a = CommandThatReturnsIntegers
$a += 4
 
If CommandThatReturnsIntegers returns 1,2,3 then everything works as expected. But what if it just returns 1?
$a = 1
$a += 4
 
Now $a is 5… oops? What just happened? Well, in essence we expected an array from CommandThatReturnsIntegers but instead it gave us an integer. Well, we could fix it such that it always returns @(1) instead of 1, then we get:
$a = @(1)
$a += 4
 
Now $a is 1,4. But what if we don't control the output of CommandThatReturnsIntegers, then we are out of luck with this approach…
 
An alternative is to use the comma operator, like this:
$a = 1
$a = $a,4
 
$a now is 1,4 as we would like it to be.
 
Let's try that with a starting array
$a = 1,2,3
$a = $a,4
 
Again, when evaluated $a is 1,2,3,4 as expected. Everything looks fine right?
 
Now try this:
$a.count
 
2? Wait a minute it evaluated to 1,2,3,4 yet the number of elements in the array is 2?
 
Let's see what we find when we look under the hood..
$a | % { "$_" }
 
Here I'm telling PowerShell to produce a string representation of the content of every element in the array. The output is:
1 2 3
4
 
So, as you may have guessed already underneath the hood $a is an array of arrays.
 
This is a common source of trouble when piping inputs to other commands. We would expect to evaluate the command on each element not each nested array.
 
As an example we might want to sum the values using:
$a | measure -Sum
 
However, this raises the exception:
Input object "System.Object[]" is not numeric.
 
But then why does $a evaluate to 1,2,3,4 when we first looked at it? The reason is that PowerShell is invoking black magic to unroll the nested array on our behalf when we evaluate it!
 
That's pretty bad and causes much deception in PowerShell-land. But there's a plus side to this, we can leverage (abuse?) PowerShell's black magic to unroll the array for us, using the following syntax:
$b = $a | % { $_ }
 
Now $b is a truly flattened array. We can test this using:
$a | % { $_ } | measure -Sum
 
Which now gives us the expected value of 10.
 
The benefit of this syntax is that it will work regardless of $a is a single element, an array or a nested array.
 
In conclusion when working with arrays take care and try to understand what goes on underneath the hood. If you end up in a situation where the array is not behaving as expected, do not rely on evaluating it in PowerShell to inspect it's structure. Instead, use $a | % { "$_" } to see what it actually holds. In case you are working with arrays and concatenation, make sure to build your command robust by unrolling the input arrays using $a | % { $_ }.