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

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

Related

Array of reference to var in powershell

Ok I guess this question has already been answered somewhere but I do not find it. So here is my few lines of codes
$a = 0
$b = 0
$c = 0
$array = #($a, $b, $c)
foreach ($var in $array) {
$var = 3
}
Write-Host "$a : $b : $c"
What I try to do is loop into $array and modify a, b and c variables to get 3 : 3 : 3 ... I find something about [ref] but I am not sure I understood how to use it.
You'll need to wrap the values in objects of a reference type (eg. a PSObject) and then assign to a property on said object:
$a = [pscustomobject]#{ Value = 0 }
$b = [pscustomobject]#{ Value = 0 }
$c = [pscustomobject]#{ Value = 0 }
$array = #($a, $b, $c)
foreach ($var in $array) {
$var.Value = 3
}
Write-Host "$($a.Value) : $($b.Value) : $($c.Value)"
Since $a and $array[0] now both contain a reference to the same object, updates to properties on either will be reflected when accessed through the other
As you mentioned you can use the [ref] keyword, it will create an object with a "Value" property and that's what you have to manipulate to set the original variables.
$a = 1
$b = 2
$c = 3
$array = #(
([ref] $a),
([ref] $b),
([ref] $c)
)
foreach ($item in $array)
{
$item.Value = 3
}
Write-Host "a: $a, b: $b, c: $c" # a: 3, b: 3, c: 3
You could also use the function Get-Variable to get variables:
$varA = Get-Variable -Name a
This way you can get more information about the variable like the name.
And if your variables have some kind of prefix you could get them all using a wildcard.
$variables = Get-Variable -Name my*
And you would get all variables that start with "my".

How does the compound assignment operator behave in child scope?

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.

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 array of arrays [duplicate]

This question already has answers here:
Powershell create array of arrays
(3 answers)
Closed 5 years ago.
This is building $ret into a long 1 dimensional array rather than an array of arrays. I need it to be an array that is populated with $subret objects. Thanks.
$ret = #()
foreach ($item in $items){
$subret = #()
$subRet = $item.Name , $item.Value
$ret += $subret
}
there might be other ways but arraylist normally works for me, in this case I would do:
$ret = New-Object System.Collections.ArrayList
and then
$ret.add($subret)
The suspected preexisting duplicate question is indeed a duplicate:
Given that + with an array as the LHS concatenates arrays, you must nest the RHS with the unary form of , (the array-construction operator) if it is an array that should be added as a single element:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = #() # create an empty *array*
foreach ($item in $items) {
$subret = $item.Name, $item.Value # use of "," implicitly creates an array
$ret += , $subret # unary "," creates a 1-item array
}
# Show result
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
This yields:
2
---
n1
v1
---
n2
v2
The reason the use of [System.Collections.ArrayList] with its .Add() method worked too - a method that is generally preferable when building large arrays - is that .Add() only accepts a single object as the item to add, irrespective of whether that object is a scalar or an array:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = New-Object System.Collections.ArrayList # create an *array list*
foreach ($item in $items) {
$subret = $item.Name, $item.Value
# .Add() appends whatever object you pass it - even an array - as a *single* element.
# Note the need for $null = to suppress output of .Add()'s return value.
$null = $ret.Add($subret)
}
# Produce sample output
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
The output is the same as above.
Edit
It is more convoluted to create an array of tuples than fill an array with PsObjects containing Name Value as the two properties.
Select the properties you want from $item then add them to the array
$item = $item | select Name, Value
$arr = #()
$arr += $item
You can reference the values in this array by doing this
foreach($obj in $arr)
{
$name = $obj.Name
$value = $obj.Value
# Do actions with the values
}

Object variable changes to a string

I have a variable set as an object as follows:
[object] $x = 'abc','def';
If I view what $x is now, I get:
acb
def
Now my problem is when I set $x to $null and then try to rather set $x from a loop using += after reading the file it change $x type to a string and if I view what $x is now it gives me:
abcdef
instead of:
abc
def
How do I go about it to keep the variable as an object rather then a string?
Below is just a sample to get the idea:
[object] $x = 'abc','def';
$x = $null;
for ($i = 0; $i -lt $numberOfColumns; $i++) {
$x += '$_.' + $NamesOfColumns[$i] + '.Trim()';
}
What you do within your code is:
[object] $x = 'abc', 'def'
==> The type of 'abc' and 'def' is [System.String]. Because you comma seperated them PowerShell does automatically create a list. So after executing that line $x is a System.Object[]. Index 0 and 1 contains [System.String].
$x = $null;
==> Now you define $null as the value for $x. So you are removing the value. The type of $x is now undefinded. You can set the value 123 then $x will become type System.Int32. You can redefine a string and so on.
Within your for-loop you use
$x += 'somestring' + $addingSomeStuff + 'otherstring'
==> The result here is that within the first Iteration of the for-loop PowerShell will assign a String to $x. So the type of $x will be [System.String]. In the next iterations the += operator adds additionally content to the value of $x, which is still [System.String]
Don't set $x to $null. Because you'll loose the type information. For more information read about the PowerShell Extended Type System.
The following snippet works. Hope that helps.
############################################
# The following was not part of your post #
# I added it to get it run in general #
$numberOfColumns = 2
$NamesOfColumns = 'Column1', 'Column2'
############################################
[object] $x = 'abc','def';
# don't set $x to $null
# define an empty list instead ;-)
$x = #()
for ($i = 0; $i -lt $numberOfColumns; $i++) {
$x += '$_.' + $NamesOfColumns[$i] + '.Trim()';
}
In the underlying type system (.NET CTS), [object] is the base class for any object, and is the most vague type description you can give any variable - it doesn't convey any specialized meaning at all.
As mentioned in #PatM0's answer, the best solution here is to initialize the variable value with the array subexpression operator (#()) before using += :
$x = #()
If you really want to force a variable to be a collection type, use the array or psobject[] type accelerators:
PS C:\> [array]$x = 'abc','def'
PS C:\> $x = $null
PS C:\> $x += 'abc'
PS C:\> $x += 'def'
PS C:\> $x
abc
def
PS C:\> [psobject[]]$x = 'abc','def'
PS C:\> $x = $null
PS C:\> $x += 'abc'
PS C:\> $x += 'def'
PS C:\> $x
abc
def
Compare with [object]:
PS C:\> [object]$x = 'abc','def'
PS C:\> $x = $null
PS C:\> $x += 'abc'
PS C:\> $x += 'def'
PS C:\> $x
abcdef