1

I found an error in one of my scripts, it looks like PowerShell (version 2.0) does not re-initialize local arrays when entering a function again. Here is a minimalist example:

function PrintNumbersUntilX {
    param (
        [ Parameter( Mandatory = $true ) ] $X
    )
    $Number = 0
    while( $Number -le $X ) {
        Write-Host $Number
        $Number++
    }
}

function PrintNumbersUntilY {
    param (
        [ Parameter( Mandatory = $true ) ] $Y
    )
    $Number = ( , 0 )
    while( $Number[ 0 ] -le $Y ) {
        Write-Host $Number[ 0 ]
        $Number[ 0 ]++
    }
}

function Main {
    Write-Host 'X-3: '
    PrintNumbersUntilX -X 3
    Write-Host 'X-12: '
    PrintNumbersUntilX -X 12
    Write-Host 'Y-3: '
    PrintNumbersUntilY -Y 3
    Write-Host 'Y-12: '
    PrintNumbersUntilY -Y 12
}

Main

Output (linebreaks replaced with space):

X-3:  0 1 2 3 X-12:  0 1 2 3 4 5 6 7 8 9 10 11 12 Y-3:  0 1 2 3 Y-12:  4 5 6 7 8 9 10 11 12

PrintNumbersUntilX works as I would expect from a local variable but PrintNumbersUntilY does not reset (reinitialize) $Number

Is this bug fixed in later versions, or is this a 'feature', if so what is it called, and how can I get rid of it?

z32a7ul
  • 3,695
  • 3
  • 21
  • 45
  • 2
    This is specific to 2.0 (repro'able in later versions by starting with `-version 2` but not otherwise), but even there it's fixed by using `$Number = @(0)` (which is the proper way of declaring an array, even if the comma trick is occasionally required to prevent unrolling in more complex cases). – Jeroen Mostert Apr 28 '21 at 12:22
  • Good point about the bug being v2-specific, @JeroenMostert, but `@(...)` isn't the proper way to declare an array in PowerShell, because it is generally unnecessary and can invite conceptual confusion - please see my answer. `, 0` isn't a trick in this case, it is the proper use of the unary form of the array-construction operator to construct a single-element array. – mklement0 Apr 28 '21 at 14:01

2 Answers2

2

You can work around it in 2.0 by using an explicit array subexpression @() to initialize $Number:

$Number = @( , 0 )
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
1

You are aware of it, but let's be clear that the behavior observed is clearly a - pretty serious bug - in version 2.0 of Windows PowerShell:

  • Seemingly, assignment of a directly constructed single-element array to a local variable is "optimized" away in subsequent calls to a given script or function.

  • If at all possible, consider upgrading your PowerShell version: v2.0 of Windows PowerShell was released in October 2009; the current version is v5.1.

    • v2.0 will definitely not see any bug fixes, whereas v5.1 - which is the last ever Windows PowerShell version - is no longer actively developed, but may see bug fixes - depending on the severity of a given bug.

    • By contrast the cross-platform PowerShell (Core) v6+ edition is Windows PowerShell's successor, which is actively developed and maintained.


Workarounds:

Option 1: Use @(...), the array-subexpression operator in lieu of the unary form of ,, the array constructor operator:

$Number = @( 0 )

This is a simplified, perhaps less obscure version of Mathias R. Jessen's solution.

To be clear:

  • Your approach to constructing the single-element array - , 0 - which works correctly in v3+, is conceptually preferable, because - unlike the , operator - @(...) doesn't actually construct arrays, it "guarantees" them, loosely speaking: That is, @( @( 0 ) ) and @( , 0 ) are the same as @( 0 ) and result in a (non-nested) single-element array. By contrast, , , 0 indeed constructs a nested array (a single-element array whose element is another single-element array whose element is 0).

  • @(...) was never meant to be used for declaring array literals: Its purpose is to collect the output from the enclosed command or expression in a newly constructed [object[]] array, even if only one object was output, so as to ensure that situationally single-object output from the pipeline is still treated as an array (given that single-object output is by default collected as-is, without an array wrapper).

    • In PowerShell versions up to v5.0, using @(...) with an array literal was not only unnecessary, but inefficient: An extra array was created behind the scenes, based on the mechanism described.

    • Because the "off-label" use of @(...) for array initialization was very widespread, v5.1 introduced an optimization: @(...) was effectively optimized away behind the scenes in expression such as @( 1, 2 ), making it effectively the same as 1, 2

    • It is precisely the absence of this optimization that makes the @( 0 ) workaround effective in v2.0: because the evaluation is deferred until runtime, the buggy assignment "optimization" isn't performed.

  • See the bottom section of this answer for more information about @(...)

Option 2: Type-constrain the variable as an array, which with strong typing [int[]] even improves efficiency (though the impact may be negligible):

[array] $Number = 0 # Note: "," isn't even needed then; constructs [object[]] array
   
# Or, more efficiently, with strong typing:
# (The above constructs an object[] array in which value types must be boxed.)
[int[]] $Number = 0
mklement0
  • 382,024
  • 64
  • 607
  • 775