Chapter 05Lesson 01~115 minutes

Arrays and Lists: Preserve One Value per Element

Model deployment targets as real collections, use indexing and array normalization safely, understand pipeline enumeration, and know when repeated growth calls for a resizable List.

Learning objectives

  • Store many deployment targets as separate array elements rather than delimiter-dependent strings.
  • Create, index, negatively index, slice, count, replace, and extend arrays while understanding fixed-size behavior.
  • Distinguish scalar values from one-element and multi-element collections and normalize with @().
  • Explain how command output becomes collections and how arrays are automatically enumerated through pipelines.
  • Recognize when repeated += growth is inefficient and when List[string] is a proportionate alternative.
  • Build a deployment-target plan that preserves spaces and special characters as part of individual values.

1. Many deployment targets should be many values, not one clever string

Suppose a deployment must target three hosts: api-01, api blue 02, and worker[canary]. A fragile approach is to concatenate them into one string and split the string later. That immediately creates questions about delimiters, spaces, brackets, escaping, and values that happen to contain the delimiter. An array solves a more fundamental problem: it preserves each target as a separate value.

$badTargets = 'api-01,api blue 02,worker[canary]'
$targets = @(
    'api-01'
    'api blue 02'
    'worker[canary]'
)

$badTargets.GetType().Name
$targets.GetType().Name
$targets.Count

The first variable contains one String. The second contains an array whose three elements are still three independent strings. That distinction becomes critical when values are passed to commands, serialized to JSON, filtered, or logged.

2. Arrays preserve ordered elements and give each element an index

An array is an ordered collection. Each element has a numeric position called an index. PowerShell uses zero-based indexing, so the first element is at index 0. You can create arrays with comma-separated expressions, multiple expressions inside @(...), ranges, or command output.

$regions = 'eu-west', 'us-east', 'ap-south'
$ports = 8080, 8081, 8082
$numbers = 1..5

$regions[0]
$regions[1]
$regions[-1]
$regions.Count
eu-west
us-east
ap-south
3

Negative indexes count from the end: -1 is the last element, -2 is the second-to-last, and so on. Count is the usual collection-size property in PowerShell.

3. Slicing asks for several positions and returns the selected elements

PowerShell lets you provide multiple indexes to retrieve a subset, sometimes called a slice. This does not change the source array; it produces the selected elements as output.

$targets = 'api-01', 'api-02', 'worker-01', 'worker-02', 'canary-01'

$targets[0..2]
$targets[1, 3, -1]

Ranges such as 0..2 are convenient when the positions are consecutive. An explicit list such as 1, 3, -1 makes non-contiguous selection obvious. In production code, prefer names and filters over hard-coded indexes when the meaning of positions could change.

4. Array elements can be replaced, but growing an array creates a new array

PowerShell arrays are fixed-size .NET arrays. You can replace an existing element because the position already exists. When you use + or += to grow an array, PowerShell creates a new array containing the old and new elements and assigns that new array back to the variable.

$targets = 'api-01', 'api-02'
$targets[1] = 'api-blue-02'
$before = $targets

$targets += 'worker-01'

$targets
$targets.Count
[object]::ReferenceEquals($before, $targets)

For a small configuration list, += is readable and usually fine. Repeating it thousands of times in a loop can be inefficient because each growth step may allocate and copy an array. Observe the workload before optimizing, but know why the pattern can become expensive.

5. Scalar versus collection shape matters at command boundaries

A scalar is one value rather than a collection of values. PowerShell often makes one-value cases convenient, but convenience can hide shape changes. A command that returns zero items, one item, or many items can produce different assignment results unless you deliberately normalize the output.

$oneTarget = 'api-01'
$forcedArray = @('api-01')

$oneTarget.GetType().Name
$forcedArray.GetType().Name
$forcedArray.Count

# @() always produces an array, even for zero or one pipeline result.
$matching = @(Get-Process -Name pwsh -ErrorAction SilentlyContinue)
"Found: $($matching.Count)"
Shape rule: Use @(...) when a caller must always receive a collection. This removes a large class of “works for many items but breaks for one item” bugs.

6. Pipeline output is automatically enumerated unless you preserve it as one value

Chapter 03 showed that pipeline commands emit objects one at a time. When multiple objects are assigned to a variable, PowerShell normally collects them into an array. When an array itself enters the pipeline, PowerShell normally enumerates it—meaning each element becomes a separate pipeline input object.

$targets = 'api-01', 'api-02', 'worker-01'

$targets | ForEach-Object {
    "Pipeline object: $_"
}

# Unary comma wraps the array as one pipeline object.
,$targets | ForEach-Object {
    "Received type: $($_.GetType().FullName); Count=$($_.Count)"
}

The unary comma was previewed in Chapter 04 because it changes collection shape. Use it only when a downstream operation truly needs the array itself as one object. Most pipelines should enumerate elements naturally.

7. Use a generic List when repeated growth is the real operation

A generic .NET List[T] is a resizable collection whose element type is declared by T. You do not need a survey of .NET collections to use the one case that matters here: repeatedly appending items while building a result.

$targets = [System.Collections.Generic.List[string]]::new()
$targets.Add('api-01')
$targets.Add('api blue 02')
$targets.Add('worker[canary]')

$targets.Count
$targets[1]
$targets | ForEach-Object { "Target: $_" }

Use an ordinary array when you already know the values or the collection is small and mostly read-only. Consider List[string] when a loop is naturally accumulating many strings. Keep the data-structure choice proportional to the problem.

8. Separate elements preserve spaces, wildcard characters, and punctuation safely

PowerShell strings can contain spaces, brackets, dollar signs, semicolons, and other characters. The key is not to smuggle multiple logical values through one string. If each target remains one array element, later code can pass it as one argument or validate it as one value.

$targets = @(
    'api blue 02'
    'worker[canary]'
    'edge$01'
    'folder name/server-01'
)

for ($i = 0; $i -lt $targets.Count; $i++) {
    "Index {0}: <{1}>" -f $i, $targets[$i]
}

The angle brackets in the output are only visual delimiters; they help you see that whitespace belongs to the element. Chapter 07 will deepen path and wildcard behavior.

9. Arrays are useful when order and repeated values are meaningful

Typical DevOps arrays include deployment targets, artifact paths, allowed regions, ports, health-check URLs, retry delays, and ordered rollout waves. Arrays can contain duplicate values and preserve order. If you instead need a fast lookup by a descriptive key such as environment name, Lesson 02 introduces hashtables. If each element needs several named fields, Lesson 03 introduces PSCustomObject.

10. Diagnose array bugs by inspecting both type and shape

When collection code behaves unexpectedly, inspect the variable instead of assuming it is an array.

$candidate = Get-Process -Name pwsh -ErrorAction SilentlyContinue

if ($null -eq $candidate) {
    'No process object was returned.'
}
else {
    $candidate.GetType().FullName
    @($candidate).Count
}

The @($candidate).Count pattern gives a stable count even when the original value is scalar. When collection shape is part of a function contract, make it explicit rather than relying on incidental command cardinality.

11. Lab: build a deployment-target plan without flattening values into text

This lab creates only in-memory values. It intentionally includes spaces and punctuation so you can verify that every target remains one element.

$rawTargets = @(
    'api-01'
    'api blue 02'
    'worker[canary]'
    'edge$01'
)

$targets = @($rawTargets | Where-Object { $_.Trim().Length -gt 0 })

$plan = for ($index = 0; $index -lt $targets.Count; $index++) {
    [pscustomobject]@{
        Wave = $index + 1
        Target = $targets[$index]
        Label = 'deploy:{0}' -f $targets[$index]
    }
}

$plan | Format-Table Wave, Target, Label -AutoSize
"Target count: $($targets.Count)"
"Last target: $($targets[-1])"

The custom objects are a preview of Lesson 03. The important lesson here is that the Target property receives the original array element unchanged; no later split operation is needed.

Verification checklist

12. Common mistakes to avoid

Joining values too early. Keep logical values separate until a human-readable string boundary is actually required.

Assuming one result is always an array. Normalize with @() when callers depend on collection shape.

Using array indexes as business meaning. Indexes are positions; prefer object properties or keys when you mean names such as “production” or “database”.

Optimizing small lists prematurely. List[T] is useful for repeated growth, but ordinary arrays remain the clearest choice for many small configurations.

13. Knowledge check

Question 1. Why is @("api-01") different from "api-01"?

Question 2. What does index -1 mean for an array?

Question 3. Why can repeated += be inefficient for a large array?

Question 4. What does @(command) guarantee?

Question 5. When is List[string] a reasonable choice?

14. Summary

Arrays model ordered groups of values without flattening them into strings. You can create, index, slice, replace, and enumerate array elements; negative indexes count from the end. @() is the key normalization tool when zero/one/many command results must have predictable collection shape. Array growth with += is clear for small data but can be inefficient when repeated heavily; a generic List[T] is a focused alternative for repeated accumulation.

15. Further reading

Next lesson

Named lookup comes next: hashtables and ordered dictionaries

Lesson 02 changes the question from “what is element 2?” to “what value belongs to this key?” and uses dictionaries for environment/configuration lookup state.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.