Array Literals In PowerShell

Array Literals In PowerShell

  • Comments 12

The first thing to understand is that there are no array literals in PowerShell J Arrays are built using operators or casts. The way to build an array in PowerShell is to use the comma operator as shown in the following examples:

 

$a = , 1          # array of one element

$a = 1,2          # array of two elements

$a = 1,2,3        # array of three elements.

$a = (1,2),(3,4)  # array of two elements, each of which is a two element array

$a += ,(5,6)      # add another element which is an array of two element.

 

Now let's display the array we've built. Simply displaying the variable isn't very helpful since PowerShell will unravel everything in the display process obscuring the inner structure. Instead we'll use individual indexing operations to look at each piece:

 

PS (61) > $a[0]

1

2

PS (62) > $a[1]

3

4

PS (63) > $a[2]

5

6

 

The comma operator is the array construction operator in PowerShell (similar to the cons function in LISP.)

 

By now, if you're familiar with PowerShell, you're asking – so what's with the @() notation? If it isn't an array literal, then what is it? First let's talk about arrays and casts. The other way to build an array is through a covariant conversions or casts. In PowerShell, this includes the ability to cast a scalar object into an array. In the following example, we're casting an integer into an array:

 

PS (64) > $a = [array] 1

PS (65) > $a[0]

1

 

Now, since it's already an array, casting it again doesn't result in a second level of nesting:

 

PS (66) > $a = [array] [array] 1

PS (67) > $a[0]

1

 

However using 2 commas *does* nest the array since it is the array construction operation:

 

PS (68) > $a = ,,1

PS (69) > $a[0][0]

1

 

Now on to @(). In essence, the @( … ) operation is syntactic sugar for

 

[array] $( … )

 

 So – if the statements in @() return a scalar, it will be wrapped in an array but if the result is already an array, then it won't be nested.

 

And finally, why did we do it this way? The goal was to make array operations consistent with the command argument syntax. A comma on the command line indicates a collection of objects bound to one parameter.

 

PS (77) > function f([array] $x, [array] $y, [array] $z) {$x.count; $y.count, $z.count}

PS (78) > foo a,b,c z l,m

3

1

2

 

And why have @() at all? Because pipelines in a value context gather their results from a stream. From the stream you can't tell whether the pipeline returned a singleton or a collection. Using the @() means that you don't really have to care. If you might receive a collection, then just use

 

$result = @( … pipeline … )

 

and it doesn't matter what you receive, it will always be an array.

 

-bruce

 

Bruce Payette [MSFT]

Windows PowerShell Tech Lead

 

Visit the Windows PowerShell Team blog at: http://blogs.msdn.com/PowerShell

Visit the Windows PowerShell ScriptCenter at: http://www.microsoft.com/technet/scriptcenter/hubs/msh.mspx

Windows PowerShell in Action (book): http://manning.com/powershell

 

Leave a Comment
  • Please add 3 and 4 and type the answer here:
  • Post
  • I suppose the big question is how do you explain this in 2 lines in the PowerShell quickstart guide.

  • OK, riddle me this.  I have a function that may return nothing, a scalar or a collection.  I will attempt to foreach the results of the function call so I would like to use the @() around the results in the function before returning them.  That way the foreach would see either A) an empty collection or B) a collection with one or more elements in it.

    Unfortunately it seems that PowerShell converts an empty array returned from a function to $null.  In this case the $null result causes the foreach to iterate and when I then try to use the loop variable in a .NET method call I get a NullReferenceException.

    So I essentially have to test for null results before performing the foreach loop. That's a bummer because it appeared as though the @() would get me out of having to do that.  :-(

  • I realise this is a judgement call, but did you find that the need to do that was so frequent that it warranted the shorthand '@' over [array], or was this pre-emptive?

    Let me try to put that another way that might sound less like a challenge. Shells have traditionally gained a very baroque syntax as they evolved, and I've been very pleased with how clean the PowerShell language is by comparison. Is that down to concious effort to avoid unnecessary syntax (in which case, I'd welcome further justification of the @ syntax, particularly any empirical observations you made), or purely a function of its age?

    In general, I'd like to know more about how the language was designed, because it sets a better context for what it does.

    By the way, your book "PowerShell in Action" costs $29.69 to preorder on amazon.com, but   £30.39 from amazon.co.uk: at US$60.02, it's twice the price for people in the UK. At that difference, I shan't be buying it. You should complain to your publisher.

  • Hmmm - Keith - this should work exactly as you described. Here's a function that returns an empty array:

    PS (48) > function empty { @() }

    Used in foreach, it executes exactly 0 times.

    PS (49) > foreach ($i in @(empty)) { "i is $i" }

    And here's the example with two statements, neither of which return anything.

    PS (50) > foreach ($i in @(write-host "Foo"; empty)) { "i is $i" }

    Foo

    An expression that returns nothing is only converted to null in a scalar context. In an array context, it is treated as an empty array.

    Can you provide an example where this fails? We may have a bug in that case.

    Thanks.

    -bruce

  • Re: the amazon UK pricing - I've contacted the publisher and they're looking into it.

    Next - about @( ... ) - sequences in PowerShell were one of the toughest design areas. Traditional shells are approximately typeless and we needed to get that sort of behaviour with PowerShell with pipes. We experimented with a variety of approaches for having the system figure things out but in the end we found that it was most effective to give the control back to the user. The @() shortcut is valuable both because it provides a concise way of handling a common scenario and because it means that you don't have to explicitly work with types. (Explicit use of types are (or were) considered a more advanced user scenario - fine for users with a programming background but rather less friendly to non-programmers.)

    And finally – regarding the design approach to the PowerShell language - the observation that shell languages become baroque through accretion of  features  is an excellent one. This is something that was very much in the forefront of our minds in designing the language. We wanted a language that could support basic command line-style operations but still scale to address complex scripting without an explosion of syntax.  Our solution was the idea of bimodal parsing – expression mode and command mode. This is still more complex than languages like Python but vastly simpler than most shells which are collections of micro-syntaxes.  In a sense,  the  PowerShell syntax is to traditional shell syntax what Java is to C++ - similar but not backwards compatible, rationalized and simplified to the best extent possible.

    -bruce

  • Bruce,

    if I type

     PS> $a = 1,2,3

     PS> $b = @($a)

    then, according to your explanation, I should have two arrays with the same content.

     PS> $a.gettype()

     IsPublic IsSerial Name     BaseType

     -------- -------- ----     --------

     True     True     Object[] System.Array

    ok so far, but

     PS> $b.gettype()

     IsPublic IsSerial Name              BaseType

     -------- -------- ----              --------

     False    True     SZArrayEnumerator System.Object

    and furthermore

     PS>  "Shouldn't be " + $a.length + " = " + $b.length + "?"

     Shouldn't be 3 = ?

    Where's my error?

    JensG

  • Here's something that frustrates me with PowerShell:

    PS> $a = @( "this is a single-element array")

    PS> $a

    this is a single-element array

    PS> $a -is [array]

    True

    PS> $a | get-member

      TypeName: System.String

    Name    MemberType   Definition

    ----    ----------   ----------

    Clone   Method       System.Object Clone()

    ...

    Now I know that I should really be able to remember the methods on an Array after being a .NET programmer for so long but I don't! I have a lousy memory and would far prefer to call get-member than fire up the MSDN library again! Why is this inconsistent with [hashtable] which behaves as I would expect?

  • Hi Bruce,

    This is an interesting behaviour I've noticed that I'm not sure was the original intention:

    I have a scenario such as the following:

    $moo = @(something_returned)

    however I have noticed that if "something_returned" is $null, i.e. we have $moo = @($null) then when I do $moo.Count, it turns out to be a single element array containing a $null value.  Not what I was expecting, rather I'd expect $moo would also be null.

    Attempting variations of the above task, the following actaully produces what I expect, the variable $moo being equal to $null

    [array]$moo = $null

    $moo = [array]$null

    Is there something weird occuring here?

    The reason I'm casting the "something_returned" to arrays is related to the problem Keith described with foreach statements entering and executing once for $null ...

    i.e

    foreach ($item in $null)

    {

      Write-Host "this shouldn't display but does"

    }

  • An array can be null or it can be empty. These are obviously two different things. An array can also have the value null as an array element. The @( ) construction retrieves anything that is emitted into the pipeline and makes sure it's wrapped in an array. Emitting $null is still emitting something so the constructed array isn't empty:

    PS (25) > @($null).count

    1

    PS (26) > @($null,$null).count

    2

    PS (27) > @($null,$null,$null).count

    3

    In a pipeline, it's possible that the pipeline won't write anything into the pipeline:

    PS (33) > @(get-process csrss).count # writes one object

    1

    PS (34) > @(get-process nosuchprocess*).count # wrote nothing so the array is empty

    0

    Alternatively, think about this way: @() is approximately equivalent to

    PS (45) > $null | foreach {$al=new-object collections.arraylist} {[void] $al.add($_)} {$al.count}

    1

    PS (46) > $null,$null | foreach {$al=new-object collections.arraylist} {[void] $al.add($_)} {$al.count}

    2

    (except, of course, you return the array instead of the count...)

    -bruce

  • Where all this gets really unintuitive is in the following case:

    PS > @() | where-object ($false) | foreach-object { "found something" }

    ...which obviously returns nothing.

    However, if you break the (empty) pipeline up, storing the result into a variable, when the variable (which contains a scaler null) gets used in a pipeline it gets turned into an array or one item.

    Thus...

    PS > $store = @() | where-object ($false)

    PS > $store | foreach-object { "found something!" }

    found something!

    The simplest solution to which is to force the variable to be an array, like so:

    PS > $store = @() | where-object ($false)

    PS > @($store) | foreach-object { "found something!" }

    .. which correctly returns nothing.

  • I've been playing around with a script that loads an XML file using the [xml] handler, and I've found an interesting bit of behavior in the way that collections of subnodes are handled depending on whether or not you use XPaths or property (dot) notation. As this shows, property notation cannot always simply be used as a shortcut for selecting nodes.

    I've spent quite a while playing around with the behavior of foreach with scalars, collections and nulls and I think I see what the issue is, and I was wondering if you could confirm my thinking on this, and maybe consider a design tradeoff of changing the behavior.

    Let's say I have a document with a <Root> root node and two <Child> nodes under that root. Here's what happens:

    PS > $myDoc = ([xml](Get-Content $docPath))

    PS > @($myDoc.SelectNodes("/Root/Child")).Count

    2

    PS > @($myDoc.Root.Child).Count

    2

    Now edit the document so there are zero <Child> nodes, reload it into the $myDoc variable, and try again:

    PS > @($myDoc.SelectNodes("/Root/Child")).Count

    0

    PS > @($myDoc.Root.Child).Count

    1

    That "1" will cause a foreach loop to run (with a null value, natch), among other things. My guess is that SelectNodes explicitly returns an empty collection, which "transcends" the @ operator correctly and is represented as a true empty collection. $myDoc.Root.Child, being a dynamically generated property, returns $null instead of an empty collection, which when @'ed becomes a collection of size 1, which is what happens when splatting any null variable.

    Is this correct? Would it make more sense for those dynamically generated properties to return empty collections instead of $null?

  • I noticed something that seems weird when building a script using a multi-dimensional ragged array.  Here's a simplified example:

    $RaggedArray = ( "the" , "quick" ) , ( "brown" )

    $RaggedArray[0][0]

    $RaggedArray[0][1]

    $RaggedArray[1][0]

    ... which produces:

    the

    quick

    b

    I was expecting the third line of output to be: "brown". Am I missing something?

Page 1 of 1 (12 items)