Object variable changes to a string - powershell

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

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".

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

Powershell's -split behaves differently depending on method parameters

If you split a string, you get a string[].
$foo = "apple,banana,coconut"
Write-Output $foo.GetType()
$foo = $foo -split ","
Write-Output $foo.GetType()
If you split a paramaterized string, you get a string.
Function Get-Foo() {
param (
[string] $foo
)
Write-Output $foo.GetType()
$foo = $foo -split ","
Write-Output $foo.GetType()
}
Why?
I researched this and didn't turn anything up; Powershell's documentation on split was largely unhelpful.
The difference is that in your first example, you never declared the variable, so you can assign it values of any type and it will accept them without coercion, altering its type to match:
PS > $foo = "apple,banana,coconut"
PS > Write-Output $foo.GetType().Name
String
PS > $foo = $foo -split ","
PS > Write-Output $foo.GetType().Name
String[]
PS > $foo = 1
PS > Write-Output $foo.GetType().Name
Int32
But once you've explicitly declared $foo's type with [string], $foo.GetType() will continue to report string, and anything you assign to it will be coerced to string. You can't change the type of a declared variable just by assigning it a new value:
PS > [string] $foo = "apple,banana,coconut"
PS > Write-Output $foo.GetType().Name
String
PS > $foo = $foo -split ","
PS > Write-Output $foo.GetType().Name
String
PS > $foo = 1
PS > Write-Output $foo.GetType().Name
String
You can change the type by re-declaring it, though. Even within a function:
PS > [Int32] $foo = $foo
PS > Write-Output $foo.GetType().Name
Int32
Note that such a redeclaration assignment will fail if the value is not interpretable as the new type:
PS > [string] $foo = "apple,banana,coconut"
PS > [Int32] $foo = $foo
Cannot convert value "apple,banana,coconut" to type "System.Int32". Error: "Input string was not in a correct format."
As far as I know, there's no way to "un-declare" a variable's type so it goes back to accepting any type of value, short of removing the variable entirely with Remove-Variable.

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

how to use a FOR loop variable to help define another variable

I am new to powershell scripts and not sure how to achieve the below:
$finalArray = #()
$tempArray0 = 'A'
$tempArray1 = 'B'
$tempArray2 = 'C'
FOR (i=0; i -eq 5; i++) {$finalArray += $tempArray[i]}
$finalArray
Output Should be:
A
B
C
If the variable name is itself variable, you'll have to use the Get-Variable cmdlet to retrieve its value:
$finalArray = #()
$tempArray0 = 'A'
$tempArray1 = 'B'
$tempArray2 = 'C'
for($i=0; $i -le 2; $i++) {
$finalArray += (Get-Variable "temparray$i" -ValueOnly)
}
$finalArray
If you want to create variables with variable names, use the New-Variable cmdlet:
$Values = 'A','B','C'
for($i = 0; $i -lt $Values.Count; $i++){
New-Variable -Name "tempvalue$i" -Value $Values[$i]
}
which would result in:
PS C:\> $tempvalue1
B
Although the above will solve the example you've presented, I can think of very few cases where you wouldn't be better of using a [hashtable] instead of variable variable names - they're usually an over-complication, and you'll end up with unnecessary code anyways because you need to calculate the variable names at least twice (during creation and again when reading the value).
From the comments, it sounds like you're trying to generate input for a password generator. This can be simplified grossly, without resorting to variable variable names:
# Create a hashtable and generate the characters
$CharArrays = #{
Letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray()
Numbers = 0..9
}
# Generate some letters for the password
$PasswordChars = $CharArrays['Letters'] |Get-Random -Count 10
# Generate a few digits
$PasswordChars += $CharArrays['Numbers'] |Get-Random -Count 4
# Shuffle them around a bit
$PasswordChars = $PasswordChars |Sort-Object {Get-Random}
# Create your password
$Password = $PasswordChars -join ''