How does the compound assignment operator behave in child scope? - powershell

What happens to the following two function? I expect they are the same.
PS C:\> $i = 5
PS C:\> function Test1 { $i += 1; $i }
PS C:\> function Test2 { $i = $i + 1; $i }
PS C:\> Test1
1 # why?
PS C:\> Test2
6
PS C:\> $i
5 # will not change
I'm aware the $i in the function has local scope, hence will not be changed in the global scope, this is intentional. This question is simply about why the following 2 assignment statements behave differently here, as far as I know, they should be equivalent.
$i = $i + 1
$i += 1

Explanation:
In Test1 the variable $i is assigned a value (using the compound assignment operator). Because you cannot change the variable from the global scope, a new local variable is created (hiding the global variable), which does not have a value initially (basically 0), which is then increased by 1. When returning the value, the local variable is used.
function Test1 {
# because it's an assignment, a new local variable
# is created (with initial value = 0)
$local:i += 1
$local:i
}
In Test2 the value of the global variable $i is used (because the variable is visible in this child scope), 1 is added, and the result (6) is assigned to a new local variable. As in Test1, the value of the local variable is returned at the end.
function Test2 {
# a new local variable is created, but because the global
# variable is visible here, its value is used initially
$local:i = $global:i + 1
$local:i
}
Read more about scopes here: about_scopes

As for ...
I want the behavior of function Test2
($i = 5)
# Results
<#
5
#>
<#
To save the time/code in calling the variable separately, you can use PowerShell
variable squeezing to assign to the variable and output to the screen at the same
time.
This is more direct if you are always going to output to the screen.
It's a choice and has no effect on what you are doing
function Test1 { ($i += 1) }
function Test2 { ($i = $i + 1) }
Test1
Test2
# Results
<#
1
6
#>
($i = 5)
# Note the small refactor in the operator
function Test1 { ($i + 1) }
function Test2 { ($i = $i + 1) }
Test1
Test2
# Results
<#
6
6
#>
This still is a scope issue, local vs global, but the above gives you the matching results in both calls.

Related

Powershell differences between hash and other variables in global and local scopes

I noticed some differences in how Powershell manages global variables when they are changed in a local scope. In particular Associative Arrays (hash) seemes natively global, meanwhile the other variables types aren'nt globals and needs the prefix $global: (not just to read a value but to change it).
Here is my example:
# global variables test in powershell
$hash = #{}; # associative array
$hash["a"] = "Antenna";
$array = #(); # array
$array += "first";
$count = 1; # numeric
$glob = 1; # numeric
# here we change the values locally
function AddValues() {
$hash["b"] = "Battle";
Write-Host "local array"
$array += "second";
$array # write only "second" so is a local variable
$local = 1;
$local += 1;
Write-Host "local sum result: $local" # here the sum result is 2 ok
Write-Host "count locally: $count" # here the value is 1 ok
$count += 1;
Write-Host "count locally: $count" # here the value is still 1!!!
$count = $count+1;
Write-Host "count locally: $count" # here is 2 !!!
$global:glob += 1;
}
# call the function
AddValues
Write-Host "hash" # here the hash is ok, has 2 values
$hash
Write-Host "array" # here the array has only "first" !!!
$array
Write-Host "count: $count" # here the value is just 1!!!
Write-Host "global: $($global:glob)" # here the value is ok (2)
How this is explained?
Thank you in advance
Scoping rules only apply to variables!
PowerShell's default behavior is:
You can transparently read variables in parent scopes
Once you write to a variable, PowerShell creates a new local copy
But when you write to a specific key in a hashtable:
$hash["b"] = "value"
... you're not actually writing anything to the variable named "hash" - the variable still holds a reference to the exact same hashtable (which now happens to have more entries)!
If, on the other hand, we'd actually assigned a new value to the $hash variable, you'd see the behavior you're expecting:
function Update-Hash {
$hash = #{"b" = "value"}
$hash
}
$hash = #{"a" = 123}
$hash # shows the a=123 entry
Update-Hash
$hash # still shows a=123 entry, variable is untouched
The same goes for arrays, as well as property expressions.
If you want to ensure that the variable reference you've obtained is indeed local (and that you're not about to modify an object from the parent scope), use the local scope modifier:
function f
{
if($false){
# Oops
$hash = #{}
}
# This will affect any $hash variable in the callers scope
$hash['key'] = 'value'
# This will, appropriately, throw an index exception
$local:hash['key'] = 'value'
}
As an illustration for excellent answer by Mathias R. Jessen:
$hash = #{}; # associative array
$hash["a"] = "Antenna";
$array = #(); # array
$array += "first";
$count = 1000; # numeric
$glob = 2000; # numeric
# here we change the values locally
function AddValues() {
$hash["b"] = "Battle";
$array += "second";
$local = 1;
$local += 1;
$count += 10;
$count = $count+100;
$global:glob += 2;
'---Variables inside function'
foreach ($varia in 'array','count','hash','glob','local') {
foreach ($scope in 'Local','Global','Script',0,1) {
Try {
Get-Variable $varia -scope $scope -ErrorAction Stop |
Select-Object Name, Value, #{Name='Scope';Expression={$scope}}
} catch {}
} }
}
# call the function
AddValues
# How this is explained?
'Variables outside function'
foreach ($varia in 'array','count','hash','glob','local') {
foreach ($scope in 'Local','Global','Script',0,1) {
Try {
Get-Variable $varia -scope $scope -ErrorAction Stop |
Select-Object Name, Value, #{Name='Scope';Expression={$scope}}
} catch {}
} }
Output shows that $array local variable is created as a string inside the function (this behaviour has something to do with the += operator and deserves more elaboration):
.\SO\61888603.ps1
---Variables inside function
Name Value Scope
---- ----- -----
array second Local
array {first} Global
array {first} Script
array second 0
array {first} 1
count 110 Local
count 1000 Global
count 1000 Script
count 110 0
count 1000 1
hash {a, b} Global
hash {a, b} Script
hash {a, b} 1
glob 2002 Global
glob 2002 Script
glob 2002 1
local 2 Local
local 2 0
Variables outside function
array {first} Local
array {first} Global
array {first} Script
array {first} 0
count 1000 Local
count 1000 Global
count 1000 Script
count 1000 0
hash {a, b} Local
hash {a, b} Global
hash {a, b} Script
hash {a, b} 0
glob 2002 Local
glob 2002 Global
glob 2002 Script
glob 2002 0

ArrayList not returning expected result

Why does this code not work?
$method = {
[System.Collections.ArrayList]$array;
for ($i=0; $i -le 50; $i++) { $array += $i }
}
Executing the scriptblock with:
&$method
Shows on the console:
1, 2, 3, 4, 5, 6
when it should print 50 numbers?
The Add() method of the ArrayList type emits the index at which a new item was inserted. Cast the expression to [void], pipe it to Out-Null or assign it to $null to supress this output:
[void]$array.Add($i)
# or
$array.Add($i) |Out-Null
# or
$null = $array.Add($i)
Avoid piping to Out-Null if you do this many times, casting or assigning is much faster than piping.
The code you posted shouldn't generate any output at all, unless you already have a variable $array defined in the parent scope. The statement
[System.Collections.ArrayList]$array
casts the value of the variable $array to the type ArrayList and echoes it. If the variable has a value in the parent scope the statement will output the value as an array list, otherwise the output will be null. The subsequent loop will not use that variable, but instead increment a new (local) variable $array. You can verify that by placing a statement $array.GetType().FullName at the end of the scriptblock. You'll get System.Int32, not System.Collections.ArrayList (or System.Object[]) as you might expect.
If you want to instantiate an ArrayList object, add numbers to it, and output that list at the end of the scriptblock, you need to change your code to something like this:
$method = {
[Collections.ArrayList]$array = #()
for ($i=0; $i -le 50; $i++) { $array += $i }
$array
}
Note the assignment operation in the first statement.
Demonstration:
PS C:\> $sb = { [Collections.ArrayList]$a; 0..50 | % { $a += $_ }; $a.GetType().FullName; $a }
PS C:\> &$sb
System.Int32
1275
PS C:\> $sb = { [Collections.ArrayList]$a = #(); 0..50 | % { $a += $_ }; $a.GetType().FullName; $a }
PS C:\> &$sb
System.Collections.ArrayList
0
1
...
49
50

Powershell function creation with Set-Item and GetNewClosure

I am attempting to build a function that can itself create functions through the Set-Item command where I pass a scriptblock for the new function to the -Value parameter of Set-Item. I'm running into an issue where using GetNewClosure on the scriptblock doesn't seem to be working and I just don't know what I'm doing wrong.
In the code below, first I'm creating a function manually (testFunc) which works as intended, in that setting $x to 2 after the function creation will not cause the function to return 2; instead it returns 1 because that was the value of $x at the time the function was created. But when I try to do the same through the make-function function, the behavior changes.
I'm sure I'm overlooking something small.
> $x = 1
> $block = {$x}.GetNewClosure()
> Set-Item function:global:testFunc -Value $block
> testFunc
1
> $x = 2
> testFunc
1 # still 1 because of GetNewClosure - this is expected behavior
> $x = 1
> function make-function { $block2 = {$x}.GetNewClosure()
Set-Item function:global:testFunc2 -Value $block2
}
> make-function
> testFunc2
1
> $x = 2
> testFunc2
2 # Why is it not returning 1 in this case?
The clue is in the MSDN docs but it is subtle:
Any local variables that are in the context of the caller are copied
into the module.
GetNewClosure() appears to capture "only" local variables i.e those from the caller's current scope. So try this:
function Make-Function {
$x = $global:x
$function:global:testFunc2 = {$x}.GetNewClosure()
}
BTW you can see what variables are captured by GetNewClosure() into the newly created dynamic module by executing:
$m = (Get-Command testFunc2).Module
& $m Get-Variable -Scope 0

Alter script variable value from inside PowerShell function

I like C# so much better than PS but this time I'm forced to write a script...
I need to alter "global" variable from inside a function without passing the variable as parameter. Is it even possible?
Here is an abstract:
$a = 0
$b = 0
$c = 0
function Increment($var1, $var2)
{
$a = $var1
$b = $var2
$c++
}
Unlike C# in PS function variable are function scope bound and they remain unchanged outside of the scope of the function if used as described above. How can I make this work so the script variable are accessed by reference?
Try using the PowerShell global variable definition:
$global:a = 0
$global:b = 0
$global:c = 0
function Increment($var1, $var2)
{
$global:a = $var1
$global:b = $var2
$global:c++
}
If you really need to modify by reference, you can use [Ref], e.g.:
$a = 1
function inc([Ref] $v) {
$v.Value++
}
inc ([Ref] $a)
$a # outputs 2

Is it possible to have variable after a loop in PowerShell?

I was wondering is it possible to have variables after a conditions, e.g. when normally a while sentence would be like that:
$i = 5
while ($i -le 5) {Write-Host $i; $i++}
But because I have so many variables in my script I thought may be I could write the variables at the end of the code so I could read the script's logic first not the variables, something like that:
while ($i -le 5) {Write-Host $i; $i++}
$i = 10
$Variable2 = 5
...
$Variable100 = 25
Powershell executes the lines in your script in order from top to bottom except where a loop, condition, or a function call make the execution jump somewhere else.
So, no, you cannot initialise a variable after you used it, that just wouldn't make sense.
Your solution here would be to move all the logic inside one or more functions, and then at the end of your script you can initialise variables and call the function(s). That does roughly what you are asking for.
You don't need to expressly declare a variable in PowerShell, if that's what you're asking. Variables that weren't defined before are automatically defined and initialized with default value the first time they're used.
However, depending on how the variable is used, you may inadvertently get different variables with the same name in different scopes:
PS C:\> function Foo { $i; $i = 23; $i }
PS C:\> Foo
23
PS C:\> $i = 42
PS C:\> $i
42
PS C:\> Foo
42
23
PS C:\> $i
42
PS C:\> function Bar { $i; $global:i = 23; $i }
PS C:\> Bar
42
23
PS C:\> $i
23
Also, if you want a variable to start with a specific value, you need to initialize it before you use it. If you have numerous initializations, you could put them into a separate PowerShell script and dot-source that second script in your "worker" script:
. 'C:\path\to\config.ps1'