Everything you wanted to know about arrays

Arrays are a fundamental language feature of most programming languages. They're a collection of values or objects that are difficult to avoid. Let's take a close look at arrays and everything they have to offer.

Note

The original version of this article appeared on the blog written by @KevinMarquette. The PowerShell team thanks Kevin for sharing this content with us. Please check out his blog at PowerShellExplained.com.

What is an array?

I'm going to start with a basic technical description of what arrays are and how they are used by most programming languages before I shift into the other ways PowerShell makes use of them.

An array is a data structure that serves as a collection of multiple items. You can iterate over the array or access individual items using an index. The array is created as a sequential chunk of memory where each value is stored right next to the other.

I'll touch on each of those details as we go.

Basic usage

Because arrays are such a basic feature of PowerShell, there is a simple syntax for working with them in PowerShell.

Create an array

An empty array can be created by using @()

PS> $data = @()
PS> $data.count
0

We can create an array and seed it with values just by placing them in the @() parentheses.

PS> $data = @('Zero','One','Two','Three')
PS> $data.count
4

PS> $data
Zero
One
Two
Three

This array has 4 items. When we call the $data variable, we see the list of our items. If it's an array of strings, then we get one line per string.

We can declare an array on multiple lines. The comma is optional in this case and generally left out.

$data = @(
    'Zero'
    'One'
    'Two'
    'Three'
)

I prefer to declare my arrays on multiple lines like that. Not only does it get easier to read when you have multiple items, it also makes it easier to compare to previous versions when using source control.

Other syntax

It's commonly understood that @() is the syntax for creating an array, but comma-separated lists work most of the time.

$data = 'Zero','One','Two','Three'

Write-Output to create arrays

One cool little trick worth mentioning is that you can use Write-Output to quickly create strings at the console.

$data = Write-Output Zero One Two Three

This is handy because you don't have to put quotes around the strings when the parameter accepts strings. I would never do this in a script but it's fair game in the console.

Accessing items

Now that you have an array with items in it, you may want to access and update those items.

Offset

To access individual items, we use the brackets [] with an offset value starting at 0. This is how we get the first item in our array:

PS> $data = 'Zero','One','Two','Three'
PS> $data[0]
Zero

The reason why we use zero here is because the first item is at the beginning of the list so we use an offset of 0 items to get to it. To get to the second item, we would need to use an offset of 1 to skip the first item.

PS> $data[1]
One

This would mean that the last item is at offset 3.

PS> $data[3]
Three

Index

Now you can see why I picked the values that I did for this example. I introduced this as an offset because that is what it really is, but this offset is more commonly referred to as an index. An index that starts at 0. For the rest of this article I will call the offset an index.

Special index tricks

In most languages, you can only specify a single number as the index and you get a single item back. PowerShell is much more flexible. You can use multiple indexes at once. By providing a list of indexes, we can select several items.

PS> $data[0,2,3]
Zero
Two
Three

The items are returned based on the order of the indexes provided. If you duplicate an index, you get that item both times.

PS> $data[3,0,3]
Three
Zero
Three

We can specify a sequence of numbers with the built-in .. operator.

PS> $data[1..3]
One
Two
Three

This works in reverse too.

PS> $data[3..1]
Three
Two
One

You can use negative index values to offset from the end. So if you need the last item in the list, you can use -1.

PS> $data[-1]
Three

One word of caution here with the .. operator. The sequence 0..-1 and -1..0 evaluate to the values 0,-1 and -1,0. It's easy to see $data[0..-1] and think it would enumerate all items if you forget this detail. $data[0..-1] gives you the same value as $data[0,-1] by giving you the first and last item in the array (and none of the other values). Here is a larger example:

PS> $a = 1,2,3,4,5,6,7,8
PS> $a[2..-1]
3
2
1
8

This is the same as:

PS> $a[2,1,0,-1]
3
2
1
8

Out of bounds

In most languages, if you try to access an index of an item that is past the end of the array, you would get some type of error or an exception. PowerShell silently returns nothing.

PS> $null -eq $data[9000]
True

Cannot index into a null array

If your variable is $null and you try to index it like an array, you get a System.Management.Automation.RuntimeException exception with the message Cannot index into a null array.

PS> $empty = $null
PS> $empty[0]
Error: Cannot index into a null array.

So make sure your arrays are not $null before you try to access elements inside them.

Count

Arrays and other collections have a count property that tells you how many items are in the array.

PS> $data.count
4

PowerShell 3.0 added a count property to most objects. you can have a single object and it should give you a count of 1.

PS> $date = Get-Date
PS> $date.count
1

Even $null has a count property except it returns 0.

PS> $null.count
0

There are some traps here that I will revisit when I cover checking for $null or empty arrays later on in this article.

Off-by-one errors

A common programming error is created because arrays start at index 0. Off-by-one errors can be introduced in two ways.

The first is by mentally thinking you want the second item and using an index of 2 and really getting the third item. Or by thinking that you have four items and you want last item, so you use the count to access the last item.

$data[ $data.count ]

PowerShell is perfectly happy to let you do that and give you exactly what item exists at index 4: $null. You should be using $data.count - 1 or the -1 that we learned about above.

PS> $data[ $data.count - 1 ]
Three

This is where you can use the -1 index to get the last element.

PS> $data[ -1 ]
Three

Lee Dailey also pointed out to me that we can use $data.GetUpperBound(0) to get the max index number.

PS> $data.GetUpperBound(0)
3
PS> $data[ $data.GetUpperBound(0) ]
Three

The second most common way is when iterating the list and not stopping at the right time. I'll revisit this when we talk about using the for loop.

Updating items

We can use the same index to update existing items in the array. This gives us direct access to update individual items.

$data[2] = 'dos'
$data[3] = 'tres'

If we try to update an item that is past the last element, then we get an Index was outside the bounds of the array. error.

PS> $data[4] = 'four'
Index was outside the bounds of the array.
At line:1 char:1
+ $data[4] = 'four'
+ ~~~~~~~~~~~~~
+ CategoryInfo          : OperationStopped: (:) [], IndexOutOfRangeException
+ FullyQualifiedErrorId : System.IndexOutOfRangeException

I'll revisit this later when I talk about how to make an array larger.

Iteration

At some point, you might need to walk or iterate the entire list and perform some action for each item in the array.

Pipeline

Arrays and the PowerShell pipeline are meant for each other. This is one of the simplest ways to process over those values. When you pass an array to a pipeline, each item inside the array is processed individually.

PS> $data = 'Zero','One','Two','Three'
PS> $data | ForEach-Object {"Item: [$PSItem]"}
Item: [Zero]
Item: [One]
Item: [Two]
Item: [Three]

If you have not seen $PSItem before, just know that it's the same thing as $_. You can use either one because they both represent the current object in the pipeline.

ForEach loop

The ForEach loop works well with collections. Using the syntax: foreach ( <variable> in <collection> )

foreach ( $node in $data )
{
    "Item: [$node]"
}

ForEach method

I tend to forget about this one but it works well for simple operations. PowerShell allows you to call .ForEach() on a collection.

PS> $data.foreach({"Item [$PSItem]"})
Item [Zero]
Item [One]
Item [Two]
Item [Three]

The .foreach() takes a parameter that is a script block. You can drop the parentheses and just provide the script block.

$data.foreach{"Item [$PSItem]"}

This is a lesser known syntax but it works just the same. This foreach method was added in PowerShell 4.0.

For loop

The for loop is used heavily in most other languages but you don't see it much in PowerShell. When you do see it, it's often in the context of walking an array.

for ( $index = 0; $index -lt $data.count; $index++)
{
    "Item: [{0}]" -f $data[$index]
}

The first thing we do is initialize an $index to 0. Then we add the condition that $index must be less than $data.count. Finally, we specify that every time we loop that we must increase the index by 1. In this case $index++ is short for $index = $index + 1. The format operator (-f) is used to insert the value of $data[$index] in the output string.

Whenever you're using a for loop, pay special attention to the condition. I used $index -lt $data.count here. It's easy to get the condition slightly wrong to get an off-by-one error in your logic. Using $index -le $data.count or $index -lt ($data.count - 1) are ever so slightly wrong. That would cause your result to process too many or too few items. This is the classic off-by-one error.

Switch loop

This is one that is easy to overlook. If you provide an array to a switch statement, it checks each item in the array.

$data = 'Zero','One','Two','Three'
switch( $data )
{
    'One'
    {
        'Tock'
    }
    'Three'
    {
        'Tock'
    }
    Default
    {
        'Tick'
    }
}
Tick
Tock
Tick
Tock

There are a lot of cool things that we can do with the switch statement. I have another article dedicated to this.

Updating values

When your array is a collection of string or integers (value types), sometimes you may want to update the values in the array as you loop over them. Most of the loops above use a variable in the loop that holds a copy of the value. If you update that variable, the original value in the array is not updated.

The exception to that statement is the for loop. If you want to walk an array and update values inside it, then the for loop is what you're looking for.

for ( $index = 0; $index -lt $data.count; $index++ )
{
    $data[$index] = "Item: [{0}]" -f $data[$index]
}

This example takes a value by index, makes a few changes, and then uses that same index to assign it back.

Arrays of Objects

So far, the only thing we've placed in an array is a value type, but arrays can also contain objects.

$data = @(
    [pscustomobject]@{FirstName='Kevin';LastName='Marquette'}
    [pscustomobject]@{FirstName='John'; LastName='Doe'}
)

Many cmdlets return collections of objects as arrays when you assign them to a variable.

$processList = Get-Process

All of the basic features we already talked about still apply to arrays of objects with a few details worth pointing out.

Accessing properties

We can use an index to access an individual item in a collection just like with value types.

PS> $data[0]

FirstName LastName
-----     ----
Kevin     Marquette

We can access and update properties directly.

PS> $data[0].FirstName

Kevin

PS> $data[0].FirstName = 'Jay'
PS> $data[0]

FirstName LastName
-----     ----
Jay       Marquette

Array properties

Normally you would have to enumerate the whole list like this to access all the properties:

PS> $data | ForEach-Object {$_.LastName}

Marquette
Doe

Or by using the Select-Object -ExpandProperty cmdlet.

PS> $data | Select-Object -ExpandProperty LastName

Marquette
Doe

But PowerShell offers us the ability to request LastName directly. PowerShell enumerates them all for us and returns a clean list.

PS> $data.LastName

Marquette
Doe

The enumeration still happens but we don't see the complexity behind it.

Where-Object filtering

This is where Where-Object comes in so we can filter and select what we want out of the array based on the properties of the object.

PS> $data | Where-Object {$_.FirstName -eq 'Kevin'}

FirstName LastName
-----     ----
Kevin     Marquette

We can write that same query to get the FirstName we are looking for.

$data | Where FirstName -eq Kevin

Where()

Arrays have a Where() method on them that allows you to specify a scriptblock for the filter.

$data.Where({$_.FirstName -eq 'Kevin'})

This feature was added in PowerShell 4.0.

Updating objects in loops

With value types, the only way to update the array is to use a for loop because we need to know the index to replace the value. We have more options with objects because they are reference types. Here is a quick example:

foreach($person in $data)
{
    $person.FirstName = 'Kevin'
}

This loop is walking every object in the $data array. Because objects are reference types, the $person variable references the exact same object that is in the array. So updates to its properties do update the original.

You still can't replace the whole object this way. If you try to assign a new object to the $person variable, you're updating the variable reference to something else that no longer points to the original object in the array. This doesn't work like you would expect:

foreach($person in $data)
{
    $person = [pscustomobject]@{
        FirstName='Kevin'
        LastName='Marquette'
    }
}

Operators

The operators in PowerShell also work on arrays. Some of them work slightly differently.

-join

The -join operator is the most obvious one so let's look at it first. I like the -join operator and use it often. It joins all elements in the array with the character or string that you specify.

PS> $data = @(1,2,3,4)
PS> $data -join '-'
1-2-3-4
PS> $data -join ','
1,2,3,4

One of the features that I like about the -join operator is that it handles single items.

PS> 1 -join '-'
1

I use this inside logging and verbose messages.

PS> $data = @(1,2,3,4)
PS> "Data is $($data -join ',')."
Data is 1,2,3,4.

-join $array

Here is a clever trick that Lee Dailey pointed out to me. If you ever want to join everything without a delimiter, instead of doing this:

PS> $data = @(1,2,3,4)
PS> $data -join $null
1234

You can use -join with the array as the parameter with no prefix. Take a look at this example to see that I'm talking about.

PS> $data = @(1,2,3,4)
PS> -join $data
1234

-replace and -split

The other operators like -replace and -split execute on each item in the array. I can't say that I have ever used them this way but here is an example.

PS> $data = @('ATX-SQL-01','ATX-SQL-02','ATX-SQL-03')
PS> $data -replace 'ATX','LAX'
LAX-SQL-01
LAX-SQL-02
LAX-SQL-03

-contains

The -contains operator allows you to check an array of values to see if it contains a specified value.

PS> $data = @('red','green','blue')
PS> $data -contains 'green'
True

-in

When you have a single value that you would like to verify matches one of several values, you can use the -in operator. The value would be on the left and the array on the right-hand side of the operator.

PS> $data = @('red','green','blue')
PS> 'green' -in $data
True

This can get expensive if the list is large. I often use a regex pattern if I'm checking more than a few values.

PS> $data = @('red','green','blue')
PS> $pattern = "^({0})$" -f ($data -join '|')
PS> $pattern
^(red|green|blue)$

PS> 'green' -match $pattern
True

-eq and -ne

Equality and arrays can get complicated. When the array is on the left side, every item gets compared. Instead of returning True, it returns the object that matches.

PS> $data = @('red','green','blue')
PS> $data -eq 'green'
green

When you use the -ne operator, we get all the values that are not equal to our value.

PS> $data = @('red','green','blue')
PS> $data -ne 'green'
red
blue

When you use this in an if() statement, a value that is returned is a True value. If no value is returned, then it's a False value. Both of these next statements evaluate to True.

$data = @('red','green','blue')
if ( $data -eq 'green' )
{
    'Green was found'
}
if ( $data -ne 'green' )
{
    'And green was not found'
}

I'll revisit this in a moment when we talk about testing for $null.

-match

The -match operator tries to match each item in the collection.

PS> $servers = @(
    'LAX-SQL-01'
    'LAX-API-01'
    'ATX-SQL-01'
    'ATX-API-01'
)
PS> $servers -match 'SQL'
LAX-SQL-01
ATX-SQL-01

When you use -match with a single value, a special variable $Matches gets populated with match info. This isn't the case when an array is processed this way.

We can take the same approach with Select-String.

$servers | Select-String SQL

I take a closer look at Select-String,-match and the $matches variable in another post called The many ways to use regex.

$null or empty

Testing for $null or empty arrays can be tricky. Here are the common traps with arrays.

At a glance, this statement looks like it should work.

if ( $array -eq $null)
{
    'Array is $null'
}

But I just went over how -eq checks each item in the array. So we can have an array of several items with a single $null value and it would evaluate to $true

$array = @('one',$null,'three')
if ( $array -eq $null)
{
    'I think Array is $null, but I would be wrong'
}

This is why it's a best practice to place the $null on the left side of the operator. This makes this scenario a non-issue.

if ( $null -eq $array )
{
    'Array actually is $null'
}

A $null array isn't the same thing as an empty array. If you know you have an array, check the count of objects in it. If the array is $null, the count is 0.

if ( $array.count -gt 0 )
{
    "Array isn't empty"
}

There is one more trap to watch out for here. You can use the count even if you have a single object, unless that object is a PSCustomObject. This is a bug that is fixed in PowerShell 6.1. That's good news, but a lot of people are still on 5.1 and need to watch out for it.

PS> $object = [PSCustomObject]@{Name='TestObject'}
PS> $object.count
$null

If you're still on PowerShell 5.1, you can wrap the object in an array before checking the count to get an accurate count.

if ( @($array).count -gt 0 )
{
    "Array isn't empty"
}

To fully play it safe, check for $null, then check the count.

if ( $null -ne $array -and @($array).count -gt 0 )
{
    "Array isn't empty"
}

All -eq

I recently saw someone on Reddit ask how to verify that every value in an array matches a given value. Reddit user u/bis had this clever solution that checks for any incorrect values and then flips the result.

$results = Test-Something
if ( -not ( $results -ne 'Passed') )
{
    'All results a Passed'
}

Adding to arrays

At this point, you're starting to wonder how to add items to an array. The quick answer is that you can't. An array is a fixed size in memory. If you need to grow it or add a single item to it, then you need to create a new array and copy all the values over from the old array. This sounds like a lot of work, however, PowerShell hides the complexity of creating the new array. PowerShell implements the addition operator (+) for arrays.

Note

PowerShell does not implement a subtraction operation. If you want a flexible alternative to an array, you need to use a generic List object.

Array addition

We can use the addition operator with arrays to create a new array. So given these two arrays:

$first = @(
    'Zero'
    'One'
)
$second = @(
    'Two'
    'Three'
)

We can add them together to get a new array.

PS> $first + $second

Zero
One
Two
Three

Plus equals +=

We can create a new array in place and add an item to it like this:

$data = @(
    'Zero'
    'One'
    'Two'
    'Three'
)
$data += 'four'

Just remember that every time you use += that you're duplicating and creating a new array. This is a not an issue for small datasets but it scales extremely poorly.

Pipeline assignment

You can assign the results of any pipeline into a variable. It's an array if it contains multiple items.

$array = 1..5 | ForEach-Object {
    "ATX-SQL-$PSItem"
}

Normally when we think of using the pipeline, we think of the typical PowerShell one-liners. We can leverage the pipeline with foreach() statements and other loops. So instead of adding items to an array in a loop, we can drop items onto the pipeline.

$array = foreach ( $node in (1..5))
{
    "ATX-SQL-$node"
}

Array Types

By default, an array in PowerShell is created as a [PSObject[]] type. This allows it to contain any type of object or value. This works because everything is inherited from the PSObject type.

Strongly typed arrays

You can create an array of any type using a similar syntax. When you create a strongly typed array, it can only contain values or objects the specified type.

PS> [int[]] $numbers = 1,2,3
PS> [int[]] $numbers2 = 'one','two','three'
ERROR: Cannot convert value "one" to type "System.Int32". Input string was not in a correct format."

PS> [string[]] $strings = 'one','two','three'

ArrayList

Adding items to an array is one of its biggest limitations, but there are a few other collections that we can turn to that solve this problem.

The ArrayList is commonly one of the first things that we think of when we need an array that is faster to work with. It acts like an object array every place that we need it, but it handles adding items quickly.

Here is how we create an ArrayList and add items to it.

$myarray = [System.Collections.ArrayList]::new()
[void]$myArray.Add('Value')

We are calling into .NET to get this type. In this case, we are using the default constructor to create it. Then we call the Add method to add an item to it.

The reason I'm using [void] at the beginning of the line is to suppress the return code. Some .NET calls do this and can create unexpected output.

If the only data that you have in your array is strings, then also take a look at using StringBuilder. It's almost the same thing but has some methods that are just for dealing with strings. The StringBuilder is specially designed for performance.

It's common to see people move to ArrayList from arrays. But it comes from a time where C# didn't have generic support. The ArrayList is deprecated in support for the generic List[]

Generic List

A generic type is a special type in C# that defines a generalized class and the user specifies the data types it uses when created. So if you want a list of numbers or strings, you would define that you want list of int or string types.

Here is how you create a List for strings.

$mylist = [System.Collections.Generic.List[string]]::new()

Or a list for numbers.

$mylist = [System.Collections.Generic.List[int]]::new()

We can cast an existing array to a list like this without creating the object first:

$mylist = [System.Collections.Generic.List[int]]@(1,2,3)

We can shorten the syntax with the using namespace statement in PowerShell 5 and newer. The using statement needs to be the first line of your script. By declaring a namespace, PowerShell lets you leave it off of the data types when you reference them.

using namespace System.Collections.Generic
$myList = [List[int]]@(1,2,3)

This makes the List much more usable.

You have a similar Add method available to you. Unlike the ArrayList, there is no return value on the Add method so we don't have to void it.

$myList.Add(10)

And we can still access the elements like other arrays.

PS> $myList[-1]
10

List[PSObject]

You can have a list of any type, but when you don't know the type of objects, you can use [List[PSObject]] to contain them.

$list = [List[PSObject]]::new()

Remove()

The ArrayList and the generic List[] both support removing items from the collection.

using namespace System.Collections.Generic
$myList = [List[string]]@('Zero','One','Two','Three')
[void]$myList.Remove("Two")
Zero
One
Three

When working with value types, it removes the first one from the list. You can call it over and over again to keep removing that value. If you have reference types, you have to provide the object that you want removed.

[list[System.Management.Automation.PSDriveInfo]]$drives = Get-PSDrive
$drives.remove($drives[2])
$delete = $drives[2]
$drives.remove($delete)

The remove method returns true if it was able to find and remove the item from the collection.

More collections

There are many other collections that can be used but these are the good generic array replacements. If you're interested in learning about more of these options, take a look at this Gist that Mark Kraus put together.

Other nuances

Now that I have covered all the major functionality, here are a few more things that I wanted to mention before I wrap this up.

Pre-sized arrays

I mentioned that you can't change the size of an array once it's created. We can create an array of a pre-determined size by calling it with the new($size) constructor.

$data = [Object[]]::new(4)
$data.count
4

Multiplying arrays

An interesting little trick is that you can multiply an array by an integer.

PS> $data = @('red','green','blue')
PS> $data * 3
red
green
blue
red
green
blue
red
green
blue

Initialize with 0

A common scenario is that you want to create an array with all zeros. If you're only going to have integers, a strongly typed array of integers defaults to all zeros.

PS> [int[]]::new(4)
0
0
0
0

We can use the multiplying trick to do this too.

PS> $data = @(0) * 4
PS> $data
0
0
0
0

The nice thing about the multiplying trick is that you can use any value. So if you would rather have 255 as your default value, this would be a good way to do it.

PS> $data = @(255) * 4
PS> $data
255
255
255
255

Nested arrays

An array inside an array is called a nested array. I don't use these much in PowerShell but I have used them more in other languages. Consider using an array of arrays when your data fits in a grid like pattern.

Here are two ways we can create a two-dimensional array.

$data = @(@(1,2,3),@(4,5,6),@(7,8,9))

$data2 = @(
    @(1,2,3),
    @(4,5,6),
    @(7,8,9)
)

The comma is very important in those examples. I gave an earlier example of a normal array on multiple lines where the comma was optional. That isn't the case with a multi-dimensional array.

The way we use the index notation changes slightly now that we've a nested array. Using the $data above, this is how we would access the value 3.

PS> $outside = 0
PS> $inside = 2
PS> $data[$outside][$inside]
3

Add a set of bracket for each level of array nesting. The first set of brackets is for the outer most array and then you work your way in from there.

Write-Output -NoEnumerate

PowerShell likes to unwrap or enumerate arrays. This is a core aspect of the way PowerShell uses the pipeline but there are times that you don't want that to happen.

I commonly pipe objects to Get-Member to learn more about them. When I pipe an array to it, it gets unwrapped and Get-Member sees the members of the array and not the actual array.

PS> $data = @('red','green','blue')
PS> $data | Get-Member
TypeName: System.String
...

To prevent that unwrap of the array, you can use Write-Output -NoEnumerate.

PS> Write-Output -NoEnumerate $data | Get-Member
TypeName: System.Object[]
...

I have a second way that's more of a hack (and I try to avoid hacks like this). You can place a comma in front of the array before you pipe it. This wraps $data into another array where it is the only element, so after the unwrapping the outer array we get back $data unwrapped.

PS> ,$data | Get-Member
TypeName: System.Object[]
...

Return an array

This unwrapping of arrays also happens when you output or return values from a function. You can still get an array if you assign the output to a variable so this isn't commonly an issue.

The catch is that you have a new array. If that is ever a problem, you can use Write-Output -NoEnumerate $array or return ,$array to work around it.

Anything else?

I know this is all a lot to take in. My hope is that you learn something from this article every time you read it and that it turns out to be a good reference for you for a long time to come. If you found this to be helpful, please share it with others you think may get value out of it.

From here, I would recommend you check out a similar post that I wrote about hashtables.