Powershell: +=, functions, and global variables

Created on 9 Dec 2019  路  16Comments  路  Source: PowerShell/PowerShell

+= seems to have trouble with global variables in functions. This is true even in PS 7. "$x = $x + 1" would work ok. I found the issue here: https://github.com/nightroman/PowerShellTraps/tree/master/Basic/Compound-assignment-operators

Steps to reproduce

$x = 1
function addone { $x += 1; $x }
addone

Expected behavior

2

Actual behavior

1

Environment data

Name                           Value
----                           -----
PSVersion                      6.2.3
PSEdition                      Core
GitCommitId                    6.2.3
OS                             Microsoft Windows 10.0.16299
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
Breaking-Change Issue-Bug

Most helpful comment

I don't see how this is by-design:

PS> function add1 { $x; $x++; $x }
[2.99ms] /Users/steve
PS> function add2 { $x; $x += 1; $x }
[2.46ms] /Users/steve
PS> function add3 { $x; $x = $x + 1; $x }
[1.47ms] /Users/steve
PS> $x = 10
[2.14ms] /Users/steve
PS> add1
10
11
[14.51ms] /Users/steve
PS> add2
10
1
[6.77ms] /Users/steve
PS> add3
10
11

All 16 comments

Just to add to this, $x++ works fine as well, but other operators similar to += do not. For example:

$x = 1
function timestwo {$x *= 2; $x}
timestwo # returns $null, should return 2

it would help if I had read the question

@jhoneill You're missing an important detail. The function returns the value of $x. The use of scope is not the issue here.

Yeah, I think this is a bug. The function appears to literally not be able to find $x (or isn't looking for it?) when using +=.

@SeeminglyScience do you know anything on this one? On the surface it looks like += is simply ignoring values from higher scopes and only looking at the local scope.

@jhoneill You're missing an important detail. The function returns the value of $x. The use of scope is not the issue here.

Read error at line 1.
I'm sorry. Thought this was a different question.

Function test {$x += 1; $local:x ; $global:x}
and
Function test {$x = $x + 1; $local:x ; $global:x}

Should do the same thing and they don't. Unless the intent was to punish the use of globals this does look like a bug, not intent.

@vexx. actually it seems worse than that. It finds X and stores it in the local scope without adding to it. Local:x being empty would be a more understandable error.

@vexx32

@SeeminglyScience do you know anything on this one? On the surface it looks like += is simply ignoring values from higher scopes and only looking at the local scope.

Yeah, all assignments do that afaik. You can read from previous scopes without issue, but attempting to write to them will create a new PSVariable in the current scope.

You can still write to previous scopes, but you would need to use Set-Variable with the Scope parameter, or be explicit in your intent with a scope modifier prefix like $global:myGlobalVar += 10.

That said, this is a little strange:

$x = 10
& { $x += 1; $x }
# returns 1 - not expected.  I would have expected 11 here.
$x
# returns 10 - expected.  Child scope shouldn't default to altering parent scope.

I would not expect that an add assignment would alter the variable from the parent scope, but I would expect that it would use the parents value for initialization.

Edit: I'm realizing the strange part I'm describing is literally what the OP is referring to, oops. Yeah looks like a bug imo. It is possible that it is by design though, someone else would have to confirm.

@jhoneill Don't think that's what's happening here.

If you create the initial $x variable differently you can see the global behaviour mirrors what we're seeing inside the function.

If a variable does not exist and you're doing += to it, it will be created and the value added:

if ($x) { Remove-Variable -Name X }

$x += 10
$x # will be 10

So it's not that it's finding $x in the global scope and just isn't adding it. It's not finding it at all, and adding the value you asked to a nonexistent value, which to PS is treated the same as if the value was 0. You can see this more clearly with @SeeminglyScience's example -- the value it uses is not the original value of $x, it's just adding the specified value from += to an empty variable.

Thanks for the clarification @SeeminglyScience! Yeah, I agree it _could_ be by design, but given that all other variable accesses tends to pull from the parent scope I think the inconsistency should probably be fixed.

@daxian-dbw, this might be something y'all wanna take a look at and sort out if we should be looking to fix. 馃槉

Yeah, this is weird. If you expand the $x += 1 out into $x = $x + 1 it behaves the way you'd expect (based on PS scoping rules).

@vexx32 it's easy to test if one uses different values, you're right.

I don't see how this is by-design:

PS> function add1 { $x; $x++; $x }
[2.99ms] /Users/steve
PS> function add2 { $x; $x += 1; $x }
[2.46ms] /Users/steve
PS> function add3 { $x; $x = $x + 1; $x }
[1.47ms] /Users/steve
PS> $x = 10
[2.14ms] /Users/steve
PS> add1
10
11
[14.51ms] /Users/steve
PS> add2
10
1
[6.77ms] /Users/steve
PS> add3
10
11

From the document @jszabo98 referenced:

From WMF 3 Release Notes.docx at WMF 3.0:

Change

Read/Modify/Write operators no longer use dynamic scoping for the Read
operation. Also, compound equality operators (including +=, -=, *=, %=, ++,
--) do not use dynamic scoping. The variable is always in the current scope.

I have vague memories about this change. @lzybkr would probably remember more. Note that the current behaviour has existed since version 3 so doing anything about it now would be a non-trivial breaking change.

IIRC, it was difficult to preserve the old behavior and both behaviors were surprising. Modifying the original example ever so slightly:

$x = 1
function addone { $x += 1; $x }
addone
$x

In V2, this prints:

2
1

In V3 and beyond, this prints:

1
1

Thus in V2, the global variable was not modified, instead a new local variable was introduced, hiding the global.

The thinking in the breaking change was that reasonable code should be written differently:

function test {
    $global:x += 1
    $local_x = $global:x + 1
}

I can sort of see why this might be desirable, but I think in terms of expectation it's more reasonable for users to expect $a += $b to be equivalent to $a = $a + $b than it is for us to expect end users to be that aware of scope lookups.

Currently, $x++ is equivalent to $x = $x + 1, but $x += 1 is equivalent to $x = 0; $x++.
Referencing a variable as in $x + 1 will look up the scope chain for the variable while assignment as in $x = will create a new variable in the current scope.

PS> $x = 1
PS> function addone1 { $x++; $x }
PS> addone1
2
PS> $Global:x
1
PS>
PS>
PS> function addone2 { $x = $x + 1; $x }
PS> addone2
2
PS> $Global:x
1
PS>
PS>
PS> function addone3 { $x += 1; $x }
PS> addone3
1
PS> $Global:x
1
PS>
PS>
PS> function addone4 { $x = 0; $x++; $x }
PS> addone4
1
PS> $Global:x
1

Ideally, $x++ and $x += 1 should have the same behavior as $x = $x + 1, but like @BrucePay said, this would be a non-trivial breaking change.

By the way, this is a nice collection of powershell traps and oddities. This is where I got the bug from: https://github.com/nightroman/PowerShellTraps

GitHub
Collection of PowerShell traps and oddities. Contribute to nightroman/PowerShellTraps development by creating an account on GitHub.

Stackoverflow, and a script that works in debug mode because of += and scope: https://stackoverflow.com/questions/61157939/powershell-error-while-adding-custom-object-to-a-list-program-runs-without-err

Stack Overflow
Trying to parse a text file using PowerShell and create a list of sections present in the file. While executing the below code snippet, error is thrown when the object is added to the list. Method
Was this page helpful?
0 / 5 - 0 ratings