Powershell values within a loop losing their value - powershell

Forgive the title, I'm not really sure how to explain what I'm seeing.
Sample Code:
$SampleValues = 1..5
$Result = "" | Select ID
$Results = #()
$SampleValues | %{
$Result.ID = $_
$Results += $Result
}
$Results
This is fairly straightforward:
Create an array with 5 numbers to be used in a loop
Create a temp variable with a NoteProperty called ID
Create an empty array to store results
Iterate through each of the 5 numbers assigning them to a temp variable then appending that to an array.
The expected result is 1,2,3,4,5 but when run this returns 5,5,5,5,5
This is a barebones example taken from a much more complex script and I'm trying to figure out why the result is what it is. In each iteration all elements that have already been added to $Results have their values updated to the most recent value. I've tested forcing everything to $Script: or $Global: scope and get the same results.
The only solution I've found is the following, which moves the $Result declaration into the loop.
$SampleValues = 1..5
$Results = #()
$SampleValues | %{
$Result = "" | Select ID
$Result.ID = $_
$Results += $Result
}
This works (you get 1,2,3,4,5 as your results). It looks like $Results is just holding multiple references to a singular $Result object but why does moving this into the loop fix the problem? In this example $Result is a string so perhaps it is creating a new object each iteration but even when I forced $Result to be an integer (which shouldn't recreate a new object since an integer isn't immutable like a string) it still fixed the problem and I got the result I expected.
If anybody has any insight into exactly why this fixes the problem I've be very curious. There are plenty of alternatives for me to implement but not understanding specifically why this works this way is bugging me.

It fixes the problem by moving into the loop because then you are then creating a new $Result object each time rather than changing a value on the same one (referenced 5 times in the array).
It doesn't have anything to do with whether you use "" | Select ID or 123 | Select ID because that just becomes a sort of property on the PSCustomObject, which is still a reference type rather than a value type.
Remember, Powershell is all .NET on the inside. Here is some C# that's analogous to what Powershell is doing in your first example that resulted in all 5s (hopefully you know C#):
var SampleValues = new []{1,2,3,4,5};
var Result = new CustomObject(){ ID = "" };
var Results = new List<Object>();
foreach (var _ in SampleValues) {
Result.ID = _;
Results.Add(Result);
}
Hopefully you can see how moving var Result = new CustomObject(){ ID = "" } inside the foreach loop would make it work better, and the same concept holds true in Powershell.

In this example $Result is a string so perhaps it is creating a new object each iteration but even when I forced $Result to be an integer (which shouldn't recreate a new object since an integer isn't immutable like a string) it still fixed the problem and I got the result I expected.
Actually, in your example $Result is not a string. It is a generic object with one propery (ID) that is a string.
Powershell variables come in 2 types - value and reference. A string or integer is passed by value, an object is passed by reference. By addind $result to $results 5 times you got 5 refereneces to the one $result object, not 5 different objects.
If we add the $result.id property (an integer / value type) to $results instead of the $result object we get:
$SampleValues = 1..5
$Result = "" | Select ID
$Results = #()
$SampleValues | %{
$Result.ID = $_
$Results += $Result.ID
}
1
2
3
4
5

Related

Enumerate through array fails

I'm enumerating through all of the datastores in our VMware environment to get names and used space.
When I run the foreach loop, it's both enumerating through the array, and not enumerating through the array.
Here's my script:
$list = #()
$row = '' | select Name, UsedSpace
$datastores = Get-Datastore
foreach ($store in $datastores) {
$row.name = $store.name;
$row.usedspace = [math]::Round(($store.extensiondata.summary.capacity - $store.extensiondata.summary.freespace)/1gb)
Write-Host $row; #To Verify that each row is different, and that enumeration is working#
$list += $row;
}
Console Output:
#{name=datastore1; usedspace=929}
#{name=datastore2; usedspace=300}
#{name=datastore3; usedspace=400}
$list variable output:
Name Usedspace
Datastore3 400
Datastore3 400
Datastore3 400
So it's enumerating through. getting all the correct data. but for some reason the line $list += $row is waiting until the last object in the array, grabs only that data, but knows that there's 3 objects in the array, and populates each index with that objects data.
The only thing I've done to troubleshoot is bounced my PowerShell console.
The reason for this is that $row is a single object. You created it once, and then you keep changing the values of its properties. When you add it to the array, you're adding a reference to it, not a copy. So the values seen will always be those that were most recently set.
Recreate your $row on every iteration of the loop.
Alternatively you could create a PSCustomObject
$list = foreach ($store in Get-Datastore) {
[PSCustomObject]#{
Name = $store.name
UsedSpace = [math]::Round(($store.extensiondata.summary.capacity -
$store.extensiondata.summary.freespace)/1gb)
}
}
$list
As mentioned in the comments, with:
$row = '' | select Name, UsedSpace; $row.GetType()
you implicitly also create an (empty) PSCustomObject,
but as this needs to be created in every iteration of the foreach and then (inefficiently) appended to $list by rebuilding the array - directly building the PSCustomObject is IMO more clear / straight forward.

Casting Object to String Array Powershell

I want to create an array of strings instead of a variable object so that I can use the "contains" keyword on each index of the array.
$myArray = Get-ADDomain
The above creates an object, which is not what I want. I also tried
[string[]] $myArray = Get-ADDomain
But after that, $myArray only contains one string and it is the first non-empty property of Get-ADDomain, in my case "ComputersContainer". What should I do to receive an array of strings where each string is a different property, such as
$myArray[0] = "AllowedDNSSuffixes = {}"
PowerShell will always return objects by design of course, and specifying that [string[]], does not really change that.
For what you are trying to use, you have to force the array creation. The below is just one way, but I am sure others will have more elegant ways of doing this as well. Though I am curious why one would want to do this, this way. But, hey, that's just me.
# Create an empty array
$DomainData = #()
# Get all the data points for the utilized cmdlet, split on a common delimiter for the array
[string[]]$DomainData = (Get-ADDomain | Select *) -split ';'
# Display the array count
$DomainData.Count
34
# validate getting a value from the array by using an index number
$Item = $DomainData[17]
NetBIOSName=CONTOSO
[array]::IndexOf($DomainData, $Item)
17
# Use that element number to validate the use of the contains comparison operator
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -contains $item){"Index key is $_ contains a value of $Item"} }
Index key is 17 contains a value of NetBIOSName=CONTOSO
# Use the previous with a partial string for a comparison, -contains cannot be used, like or match has to be used
# From the documentation:
# -Contains
# Description: Containment operator. Tells whether a collection of reference values includes a single test value.
$Item = '*domain*'
0..($DomainData.Count - 1) | %{ If($DomainData[$_] -like $item){"Index key is $_ like a value of $Item"} }
Index key is 1 like a value of *domain*
Index key is 6 like a value of *domain*
Index key is 7 like a value of *domain*
Index key is 8 like a value of *domain*
Index key is 18 like a value of *domain*
Index key is 20 like a value of *domain*
You cannot cast a PSObject directly to a string array like that.
However, this can be accomplished rather easily.
To get an array of string from the object
$myArray = Get-ADDomain
# You can use a standard array #() but these tends to be slower for bigger amount of data
$outArray = New-Object -TypeName System.Collections.Generic.List[String]
#To add just the value
$myArray.psobject.properties | Foreach { $outArray.Add($_.Value) }
# To add Name = {Value} instead
$myArray.psobject.properties | Foreach { $outArray.Add("$($_.Name) = {$($_.Value)}") }
Using an hasthable instead:
$myArray = Get-ADDomain
$hashtable = #{}
$myArray.psobject.properties | Foreach { $hashtable[$_.Name] = $_.Value }
# If you need to do something with the key
Foreach ($key in $hashtable.Keys) {
$Value = $hashtable[$key]
if ($value -like '*prod*') {
Write-Host $key
}
}

Unable to remove item from hash table

In Powershell, I have a hash table that contains data similar to this -
Name Value
---- -----
1-update.bat 1
2-update.bat 2
3-update.bat 3
3.1-update.bat 3.1
4-update.bat 4
I also have an variable that contians a number, for example 3
What I would like to do is loop through the array and remove any entry where the value is less than or equal to 3
I'm thinking that this will be easy, especially as the docs say that has tables contain a .remove method. However, the code I have below fails, yeilding this error -
Exception calling "Remove" with "1" argument(s): "Collection was of a
fixed size."
Here is the code that I used -
$versions = #{}
$updateFiles | ForEach-Object {
$versions.Add($_.name, [decimal]($_.name -split '-')[0])
}
[decimal]$lastUpdate = Get-Content $directory\$updatesFile
$versions | ForEach-Object {
if ( $_.Value -le $lastUpdate ) {
$versions.Remove($version.Name)
}
}
I first tried to loop $versions in a different manner, trying both the foreach and for approaches, but both failed in the same manner.
I also tried to create a temporary array to hold the name of the versions to remove, and then looping that array to remove them, but that also failed.
Next I hit Google, and while I can find several similar questions, none that answer my specific question. Mostly they suggest using a list (New-Object System.Collections.Generic.List[System.Object]), whcih from what I can tell is of no help to me here.
Is anyone able to suggest a fix?
Here you go, you can use .Remove(), you just need a clone of the hashtable so that it will let you remove items as you enumerate.
[hashtable]$ht = #{ '1-update.bat'=1;'2-update.bat'=2;'3-update.bat'=3;'3.1-update.bat'=3.1; '4-update.bat'=4 }
#Clone so that we can remove items as we're enumerating
$ht2 = $ht.Clone()
foreach($k in $ht.GetEnumerator()){
if([decimal]$k.Value -le 3){
#notice, deleting from clone, then return clone at the end
$ht2.Remove($k.Key)
}
}
$ht2
Notice I've cast the original variable too so that it's explicitly a hash table, may not be required, but I like to do it to at least keep things clear.
It looks like you just confused ForEach-Object with foreach but only halfway (maybe it was foreach before and you converted it).
You can't send a [hashtable] directly to ForEach-Object; the $_ in that case will just refer to the single [hashtable] you sent in. You can do:
foreach ($version in $versions.GetEnumerator()) {
$version.Name
$version.Value
}
or you can do something like this:
$versions.Keys | ForEach-Object {
$_ # the name/key
$versions[$_] # the value
$versions.$_ # also the value
}
$ht.Keys # list all keys
$ht[$_] # take an element of hastable
$ht.Remove($_) # remove an element of hastable by his key
what you want:
$ht.Keys | ? { $ht[$_] -le 3 } | %{$ht.Remove($_) }
You need to create a temporary array to hold the name/key of the versions to remove, and then looping that array to remove them from hash table:
$versionKeysToRemove = $versions.Keys | Where-Object { $versions[$_] -le $lastUpdate }
$versionKeysToRemove | ForEach-Object { $versions.Remove($_) }
Or shorter:
($versions.Keys | ? { $versions[$_] -le $lastUpdate }) | % { $versions.Remove($_) }
Please note the parentheses.

Powershell array of arrays loop process

I need help with loop processing an array of arrays. I have finally figured out how to do it, and I am doing it as such...
$serverList = $1Servers,$2Servers,$3Servers,$4Servers,$5Servers
$serverList | % {
% {
Write-Host $_
}
}
I can't get it to process correctly. What I'd like to do is create a CSV from each array, and title the lists accordingly. So 1Servers.csv, 2Servers.csv, etc... The thing I can not figure out is how to get the original array name into the filename. Is there a variable that holds the list object name that can be accessed within the loop? Do I need to just do a separate single loop for each list?
You can try :
$1Servers = "Mach1","Mach2"
$2Servers = "Mach3","Mach4"
$serverList = $1Servers,$2Servers
$serverList | % {$i=0}{$i+=1;$_ | % {New-Object -Property #{"Name"=$_} -TypeName PsCustomObject} |Export-Csv "c:\temp\$($i)Servers.csv" -NoTypeInformation }
I take each list, and create new objects that I export in a CSV file. The way I create the file name is not so nice, I don't take the var name I just recreate it, so if your list is not sorted it will not work.
It would perhaps be more efficient if you store your servers in a hash table :
$1Servers = #{Name="1Servers"; Computers="Mach1","Mach2"}
$2Servers = #{Name="2Servers"; Computers="Mach3","Mach4"}
$serverList = $1Servers,$2Servers
$serverList | % {$name=$_.name;$_.computers | % {New-Object -Property #{"Name"=$_} -TypeName PsCustomObject} |Export-Csv "c:\temp\$($name).csv" -NoTypeInformation }
Much like JPBlanc's answer, I kinda have to kludge the filename... (FWIW, I can't see how you can get that out of the array itself).
I did this example w/ foreach instead of foreach-object (%). Since you have actual variable names you can address w/ foreach, it seems a little cleaner, if nothing else, and hopefully a little easier to read/maintain:
$1Servers = "apple.contoso.com","orange.contoso.com"
$2Servers = "peach.contoso.com","cherry.contoso.com"
$serverList = $1Servers,$2Servers
$counter = 1
foreach ( $list in $serverList ) {
$fileName = "{0}Servers.csv" -f $counter++
"FileName: $fileName"
foreach ( $server in $list ) {
"-- ServerName: $server"
}
}
I was able to resolve this issue myself. Because I wasn't able to get the object name through, I just changed the nature of the object. So now my server lists consist of two columns, one of which is the name of the list itself.
So...
$1Servers = += [pscustomobject] #{
Servername = $entry.Servername
Domain = $entry.Domain
}
Then...
$serverList = $usaServers,$devsubServers,$wtencServers,$wtenclvServers,$pcidevServers
Then I am able to use that second column to name the lists within my foreach loop.

ArrayList Unrolling

Powershell unrolling is driving me crazy.
I have the following code to retrieve email addresses from an exchange recipient. I'm using the ArrayList because it is suggested by many people when you want the ability to remove items from the array.
$aliases = New-Object System.Collections.ArrayList
$smtpAddresses = (Get-Recipient $this.DN).EmailAddresses | ?{$_.Prefix.ToString() -eq 'smtp' }
foreach ($smtpAddress in $smtpAddresses) {
$aliases.Add($smtpAddress.SmtpAddress)
}
return $aliases
The value of $aliases is correct at the end of the function (i.e. will contain x email addresses and is type ArrayList) but after returning it becomes System.Object[] and has 2x entries. There x Int32's followed by x Strings (i.e. {0, 1, bob#here, bob#there} ). Why does this happen and how to I keep my ArrayList intact? Am I wrong for using ArrayList?
Out of curiosity, with all the questions/problems resulting from PS unrolling, what is its purpose? The big benefit of powershell is that you work directly with objects instead of their textual projections, unfortunately, I never know what kind of object I'm working with - and even when I check, it doesn't seem to hold its shape for more than a few lines of code.
-- Edit
The function is called as part of a PSObject
$get_aliases = { ... }
$obj | Add-Member -MemberType ScriptProperty -Name Aliases -Value $get_aliases -SecondValue $set_aliases
Part of the problem is how the array is being used inside the function. Remember, a function in PowerShell doesn't actually return anything. It writes objects to the pipeline. Therefore, the return is superfluous, but not actually causing any problems. The use of the Add function is causing the problem because Add returns the index at which the value was added and therefore writes to the pipeline as well.
function get-myarray
{
$al = New-Object System.Collections.ArrayList
$al.Add( 0 )
$al.Add( 1 )
$al.Add( 'me#co.com' )
$al.Add( 'you.co.com' )
return $al
}
$array = get-myarray
$array.Count
8
Note how the size is 8. What needs to be done is to suppress the writing of what is returned by the Add function. There are a few ways to do this but here is one:
function get-myarray
{
$al = New-Object System.Collections.ArrayList
$al.Add( 0 ) | out-null
$al.Add( 1 ) | out-null
$al.Add( 'me#co.com' ) | out-null
$al.Add( 'you.co.com' ) | out-null
return $al
}
$array = get-myarray
$array.Count
4
I don't believe the use of `ArrayList' is a wrong one if you want to remove items from it.
As far as unrolling goes, this deserves a whole other question and has been already addressed.
How are you return the $aliases?
like this?
$a = MyFunctionName # In this way `$a` is of type [OBJECT[]]
You can try this way:
[System.Collections.ArrayList]$a = MyFunctionName
after you can know the type in this way:
$a.item(0).gettype()