Arrays are a fundamental language feature of most programming languages. They are a collection of values or objects and are therefore, difficult to avoid. Let’s take a close look at arrays and everything they have to offer.
Page Index
- Page Index
- What is an array?
- Basic usage
- Arrays of Objects
- Operators
- $null or empty
- Adding to arrays
- Array Types
- Other nuances
- Anything else?
What is an array?
I am 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 with @()
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 is an array of strings, then we will have 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 uses 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 will 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 will be returned based on the order of the indexes provided. If you duplicate an index, you will 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 negitive 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).
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 will silently give you 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 will get a System.Management.Automation.RuntimeException
exception with the message Cannot index into a null array
.
PS> $empty = $null
SP> $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. An off by one error can be introduced in two very common ways.
The first is by mentally thinking you want the 2nd item and using an index of 2
and really getting the third item. Or by thinking that you have 4
items and you want last item, so you will just use the size 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)
Three
The second most common way is when iterating the list and just 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 will 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 its 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 collections.
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 is 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 me must increase the index by 1
. In this case $index++
is short for $index = $index + 1
.
Whenever you are using a for
loop, pay special attention to the condition. I used $index -lt $data.count
here. It is very 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 very easy to overlook. If you provide an array to a switch statement, it will match on each item in the array.
$data = 'Zero','One','Two','Three'
switch( $data )
{
'One'
{
'Tock'
}
'Three'
{
'Tock'
}
Default
{
'Tick'
}
}
This will produce this output:
Tick
Tock
Tick
Tock
There are a lot of cool things like this 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 will 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 are wanting to walk an array and update values inside it, then the for
loop is what you are looking for.
for ( $index = 0; $index -lt $data.count; $index++ )
{
$data[$index] = "Item: [{0}]" -f $data[$index]
}
This examples 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 have 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 will enumerate them all for us and give us 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 like this 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 it’s properties will 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 are updating the variable reference to something else that no longer points to the original object in the array. This will not 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 we will look at it first. I like the -join
operator and use it often. It will join all elements in the array with a 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 am talking about.
PS> $data = @(1,2,3,4)
PS> -join $data
1234
-replace and -split
The other operators like -replace
and -split
will 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 will allow 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 operation.
PS> $data = @('red','green','blue')
PS> 'green' -in $data
True
This can get expensive if the list is large. I will often use a regex pattern if I am checking more than 4-5 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 will get compared. Instead of returning True
, it will return the object that matches.
PS> $data = @('red','green','blue')
PS> $data -eq 'green'
green
When you use the -ne
operator, we will 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 is a False
value. Both of these next statements will 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 will try 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 is not 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 is not 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 will be 0
.
if ( $array.count -gt 0 )
{
'Array is not 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 are 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 is not empty'
}
To fully play it safe, check for $null
, then check the count.
if ( $null -ne $array -and @($array).count -gt 0 )
{
'Array is not empty'
}
All -eq
I recently saw someone 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 expensive and like a lot of work, however, PowerShell hides the complexity of creating the new array.
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 are 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 will be 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"
}
By assigning the results of the foreach
to a variable, we capture all the objects and create a single array.
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 am using [void]
at the beginning of the line is to suppress the return code. Some .Net calls will 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 is almost the same thing but has some methods that are just for dealing with strings. The StringBuilder
is specially designed for performance.
It is really common to see people move to ArrayList
from arrays. But it comes from a time where C# did not have generic support. The ArrayList
is depreciated in support for the generic List[]
Generic List
A generic type is a special type in C# that define a generalized class and the user will specify the data types it will use 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 a little bit 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 will let you leave it off of the datatypes 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 will remove 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 will return 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 are 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 is 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 are only going to have integers, a strongly typed array of integers will default 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 2 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 is not the case with a multi-dimensional array.
The way we use the index notation changes slightly now that we have 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-Object -NoEnumerate
.
PS> Write-Output -NoEnumerate $data | Get-Member
TypeName: System.Object[]
...
I have a second way to do this 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.
PS> ,$data | Get-Member
TypeName: System.Object[]
...
Return an array
This un-wrapping 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 is not commonly an issue.
The catch is that you will 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 will learn something from this article every time you read it and that it will turn 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 that you think will get value out of it.
From here, I would recommend you check out a similar post that I wrote about hashtables.