Thursday, March 11, 2010

3 PowerShell Array Gotchas

I love PowerShell, and use it for pretty much everything that I’m not forced to compile. As a result I’ve got fairly competent, and people have suggested to me that I should pull my finger out and do more blogging about PowerShell tricks and tips and suchlike. And they are right.

As a first pass, here are 3 PowerShell gotchas, all which revolve around array handling. PowerShell does some funky stuff here to make certain command line operations more intuitive, which can easily throw you if you are still thinking C# [1]

Converting Function Arguments To An Array By Mistake

When you call a .Net method, you use the familiar obj.Method(arg1,arg2) syntax, with a comma between each parameter.

However when you call a script function (or a cmdlet), you must omit the commas. If you don’t, you pass your arguments as an array to the first parameter, and many times the resulting error won’t immediately tip you off.

PS > function DumpArgs($one,$two){ write-host "One: " $one; write-host "Two: " $two }

PS > dumpargs 1 2 # Correct
One: 1
Two: 2

PS > dumpargs 1,2 # Incorrectly pass both to 1st parameter
One: 1 2
Two:


Subtle, yes? Anyone ever who has ever done any PowerShell ever has done this at least once. Ever.



Passing An Array to a .Net Constructor or Method



When you call a .Net method, as described above, that familiar comma syntax is actually still creating an array from your arguments, it’s just that’s what PowerShell uses to call .Net methods (and ctors) via the reflection APIs.



So there is a reverse gotcha. How do you call a .Net method (or ctor) that takes an array as its single parameter? Whether you create an array in-line (using comma syntax) or up-front as a variable, you will likely be told ‘Cannot find an overload for "xxx" and the argument count: "n"’, as PowerShell fails to find a method with the same number of parameters as the length of your array:



PS > $bytes = [byte[]]0x1,0x2,0x3,0x4,0x5,0x6
PS > $stream = new-object System.IO.MemoryStream $bytes
New-Object : Cannot find an overload for "MemoryStream" and the argument count: "6".


If you make the byte array smaller you’ll get other errors, as the length matches one of the ctor overloads, but not the types, or you may get semantic errors when the values bind to an overload, but fail imperative validation logic. eg:



PS > $bytes = [byte[]]0x1,0x2,0x3
PS > $stream = new-object System.IO.MemoryStream $bytes
New-Object : Exception calling ".ctor" with "3" argument(s): "Offset and length were out of bounds…


Worst of all, sometimes it can ‘work’ but not in the way you intended.

As an exercise, imagine what would happen if the array was 0x1,0x0,0x1 [3].



The (somewhat counter-intuitive) solution here is to wrap the array – in an array. This is easily done using the ‘comma’ syntax (a comma before any variable creates a length-1 array containing the variable):



PS > $bytes = 0x1,0x2,0x3,0x4,0x5,0x6
PS > $stream = new-object System.IO.MemoryStream (,$bytes)
PS > $stream.length
6


Indexing into a String, Expecting a String[]



PowerShell love to unpack things: it’s like kids at Christmas. So if a function ‘returns’ a collection with only one item in it (only one line in the file, or one file in the directory) you will get the item back, and not the collection.



Since a string itself can be indexed (as if it were char[]), this can lead to weird behaviour:



PS C:\Users\Piers> (dir .\Links | % { $_.Name })[0]
Desktop.lnk
PS C:\Users\Piers> (dir .\Links\desk* | % { $_.Name })[0]
D


In the first case the indexer retrieves the first file name as expected. However in the second only one item matched the wildcard. As a result we didn’t get back an array, but an item (the string name), and that's what we indexed into (yielding the first character). Not what we wanted.



The easy fix is to always use @ to ensure an expression produces an array, even if it only evaluates to a single item:



PS C:\Users\Piers> @(dir .\Links\desk* | % { $_.Name })[0]
Desktop.lnk


(NB: This is different from the ‘comma’ syntax described above that always introduces a parent array)



Bonus: Enumerating Collections of Arrays Without Unpacking



On a similar note, if you have a collection of arrays, and you pipe it, you will only ‘see’ the individual items, not the arrays. Again, the answer here is to wrap the arrays in arrays: only one level of unravelling is performed.



Bonus: Enumerating a Hashtable



By contrast, Hashtables don’t unravel automatically, though you might imagine they do. For example:



PS > $items = @{One=1;Two=2;Three=3}
PS > $items | % { $_ }

Name Value
---- -----
Two 2
Three 3
One 1

PS > # and yet...
PS > $items | % { $_.Name }
PS > # returns nothing.
PS > $items | % { $_.ToString() }
System.Collections.Hashtable


We're not actually enumerating the hashtable's *contents*, rather we are enumerating the hashtable as if it were a single item in a list. It just has very specific default rendering behaviour (which is why we see the contents spat out).



This normally happens for non-IEnumerable types, but presumably happens deliberately for Hashtable (which is enumerable) because it's quite 'special' within PowerShell. Anyway, to get round this you have to make the enumeration explicit:



PS > $items.GetEnumerator() | % { $_.Name }
Two
Three
One


Enough Bonus Already!



 



[1] In Hanselminutes 200 [2], Jon Skeet makes the point that C# and Java are – syntax-wise – similar enough for it to be confusing, as opposed to obvious ‘in your face’ transitions (eg between Java and VB#). I had the same experience when I travelled to the USA having spent 6 months in South America: when everyone was speaking Spanish you expected things to be different, but somehow in the US because they spoke the same language (nearly) my guard was down, and so every so often you’d be totally thrown by something being different. Anyway, I recon it’s like that between C# and PowerShell. It’s .Net and has {}’s so you are lured into a false sense of security and end up with trap all over your face. Or something.



[2] What is wrong with this URL? For show 200. That just freaks me out.



[3] No, the answer’s not down here. It’s an exercise for the reader.

7 comments:

No One Of Consequence said...

If you do a lot with PowerShell, you may want to check out PowerWF - it is basically Visual PowerShell - drag and drop PowerShell commands, set properties and go. Since properties are set directly it can avert some of your "gotchas". As a bonus, the visual scripts can be exported as PowerShell Snap-ins and Modules or command-line executables.

piers7 said...

There's a good write-up on some of the hashtable oddities here:

http://huddledmasses.org/powershell-and-hashtable-oddities/

Anonymous said...

I was scratching my head about a cmdlet that returned a single item instead of a length-1 array. would've taken me forever to figure out if I hadn't stumbled upon this post.
thanks a lot!

Anonymous said...

Awesome. You saved my day. I was calling a function using comm and was puzzled what is happening.

Thank You Thank You

Rudy Schockaert said...

Passing An Array to a .Net Constructor or Method:

How would you pass an array to a function that takes two parameters, one of which is a string:

$myObject.SetProperty("Prop1", (,$Array) ) does not work.

piers7 said...

@Rudy: passing an array as one of multiple arguments is easy:

$myObject.SetProperty("Prop1", $Array)

think of it as:
$argsArray = "Prop1",$array
$myObject.SetProperty($argsArray)

Rhys Edwards said...

This post just saved me 3 lines of code! I knew there had to be a way to deal with $Error with either one element or multiple elements, without iffing and elseing whether count was 1 or not...thank you so much!

Popular Posts