Error when modifying object that is not being enumerated - loops

I have the following script:
$serverList = #{
"Server1Name" = #{ "WindowsService1" = "Status"; "WindowsService2" = "Status" };
"Server2Name" = #{ "WindowsService1" = "Status"; "WindowsService2" = "Status" };
"Server3Name" = #{ "WindowsService1" = "Status" };
"Server4Name" = #{ "WindowsService1" = "Status" };
"Server5Name" = #{ "WindowsService1" = "Status" };
"Server6Name" = #{ "WindowsService1" = "Status" }
}
$copy = $serverList.Clone()
foreach ($server in $copy.Keys) {
foreach ($service in $copy[$server].Keys) {
$serviceInfo = Get-Service -ComputerName $server -Name $service
$serverList[$server][$service] = $serviceInfo.Status
}
}
I made sure that I am not modifying the hashtable that is being enumerated, but yet I still get this error when I run the script:
Collection was modified; enumeration operation may not execute.At line:14 char:14
+ foreach ($service in $copy[$server].Keys) {
+ ~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], InvalidOperationException
+ FullyQualifiedErrorId : System.InvalidOperationException
I read up on this here: http://blog.matticus.net/2013/11/powershell-clearing-values-within.html. If I copy the code form there, it executes without error for me.
Could my problem have something to do with nested foreach loops? Is there a mistake in my code? Can anyone shed any light on this?

Powershell does not like that you are modifying the collection which you are iterating over.
In the beginning you made a clone called $copy to avoid this problem. The clone() is a "shallow copy", thus the objects being refered to for each key are the same in your copy.
On this line:
$serverList[$server][$service] = $serviceInfo.Status
You modify the inner collection - which you are currently iterating over.
In fact, the outter collection is never modified, only referred to, so the outter clone() call is unneccessary. Instead, you should clone the inner collection.
Something like this (untested):
$serverList = #{
"Server1Name" = #{ "WindowsService1" = "Status"; "WindowsService2" = "Status" };
"Server2Name" = #{ "WindowsService1" = "Status"; "WindowsService2" = "Status" };
"Server3Name" = #{ "WindowsService1" = "Status" };
"Server4Name" = #{ "WindowsService1" = "Status" };
"Server5Name" = #{ "WindowsService1" = "Status" };
"Server6Name" = #{ "WindowsService1" = "Status" }
}
foreach ($server in $serverList.Keys) {
$copy = $serverList[$server].clone();
foreach ($service in $copy.Keys) {
$serviceInfo = Get-Service -ComputerName $server -Name $service
$serverList[$server][$service] = $serviceInfo.Status
}
}

I was surprised that the .Clone() method just creates a new reference to the same object, it does not create a new object with the same properties. I couldn't find an easy way to actually copy an entire hashtable, rather than cloning it. So I wrote a function to do this:
Function Copy-HashTable($HashTable) {
$newHash = #{}
$HashTable.GetEnumerator() | ForEach-Object {
if ($_.Value -is "Hashtable") {
$newHash[$_.Key] = Copy-HashTable $_.Value
} else {
$newHash[$_.Key] = $_.Value
}
}
$newHash
}
Applying this to your code, you would just need to replace the line
$copy = $serverList.Clone()
with
$copy = Copy-HashTable $ServerList

Related

PowerShell problem with array and grouping objects

I am having problems to group an array of objects by Environment. The following script show three environments with servers in each one. Now, I am trying to loop those objects by groups but getting errors with the Environment names. The following is my sample logic:
$array = #(("DEVELOPMENT", ("SRV1")), ("QA", ("SRV2", "SRV3")), ("PRODUCTION", ("SRV4", "SRV5")))
ForEach ($system in $array) {
$envName = $array[0]
ForEach ($hostname in $system[1]) {
Write-Host ("Result for " + $hostname + " in " + $envName)
}
}
The variable name $envName always returns the same wrong result.
[0]:"DEVELOPMENT"
[1]:"SRV1"
How can I group and loop $array[0] and $system[1] in the following way?
DEVELOPMENT = SRV1
QA = SRV2, SRV3
PRODUCTION = SRV4, SRV5
Another option would be to create an object array instead of an embedded string array.
class Server
{
[string]$Environment
[string]$ServerName
}
$Development = #("SRV1")
$Qa = #("SRV2", "SRV3")
$Production = #("SRV4", "SRV5")
$Array = #()
foreach ($System in $Development)
{
$Server = [Server]::new()
$Server.ServerName = $System
$Server.Environment = "Development"
$Array += $Server
}
foreach ($System in $Qa)
{
$Server = [Server]::new()
$Server.ServerName = $System
$Server.Environment = "QA"
$Array += $Server
}
foreach ($System in $Production)
{
$Server = [Server]::new()
$Server.ServerName = $System
$Server.Environment = "Production"
$Array += $Server
}
foreach ($System in $Array)
{
Write-Host "Result for $($System.ServerName) in $($System.Environment)"
}
The solution in the comments
"Instead of $envName = $array[0] you should write $envName = $system[0]"

Powershell: PSCustomObject array as parameter in function gets changed unexpectedly

In the simplified PS code below, I don't understand why the $MyPeople array gets changed after calling the changeData function. This array variable should just be made a copy of, and I expect the function to return another array variable into $UpdatedPeople and not touch $MyPeople:
function changeData {
Param ([PSCustomObject[]]$people)
$changed_people = $people
$changed_people[0].Name = "NEW NAME"
return $changed_people
}
# Original data:
$Person1 = [PSCustomObject]#{
Name = "First Person"
ID = 1
}
$Person2 = [PSCustomObject]#{
Name = "Second Person"
ID = 2
}
$MyPeople = $Person1,$Person2
"`$MyPeople[0] =`t`t" + $MyPeople[0]
"`n# Updating data..."
$UpdatedPeople = changeData($MyPeople)
"`$UpdatedPeople[0] =`t" + $UpdatedPeople[0]
"`$MyPeople[0] =`t`t" + $MyPeople[0]
Console output:
$MyPeople[0] = #{Name=First Person; ID=1}
# Updating data...
$UpdatedPeople[0] = #{Name=NEW NAME; ID=1}
$MyPeople[0] = #{Name=NEW NAME; ID=1}
Thanks!
PSObject2 = PSObject1 is not a copy but a reference. You need to clone or copy the original object using a method designed for that purpose.
function changeData {
Param ([PSCustomObject[]]$people)
$changed_people = $people | Foreach-Object {$_.PSObject.Copy()}
$changed_people[0].Name = "NEW NAME"
return $changed_people
}
The technique above is simplistic and should work here. However, it is not a deep clone. So if your psobject properties contain other psobjects, you will need to look into doing a deep clone.
We can clone the PSCustomObject. We will create a new PSObject and enumerate through the psobject given as parameter and add them one by one to the shallow copy.
function changeData {
Param ([PSCustomObject[]]$people)
$changed_people = New-Object PSobject -Property #{}
$people.psobject.properties | ForEach {
$changed_people | Add-Member -MemberType $_.MemberType -Name $_.Name -Value $_.Value
}
$changed_people[0].Name = 'NEW NAME'
return $changed_people
}
Or use another method by #AdminOfThings

Filter TreeView Nodes in PowerShell

I have a ton of Nodes in my TreeView, and have a textbox that filters through them to highlight the matched search. However, its a bit messy as it shows all the other nodes, and after I change my search, it leaves all nodes expanded.
I am trying to make something like this, https://www.codeproject.com/Tips/1000621/Filtering-and-Hiding-Tree-Nodes-WinForms
But I am using Windows forms / Powershell ISE and seem to struggle with implementing it into my own code.
For closing nodes I tried using things along the line of (Textbox.textlength -eq 0) to trigger a close all nodes function, but that was not working.
Here is what I want it too look like. Left is what I want, Right is what mine looks like.
Here is an example of the search function I am using.
Add-Type -AssemblyName System.Windows.Forms
function GetNodes([System.Windows.Forms.TreeNodeCollection] $nodes)
{
foreach ($n in $nodes) {
$n
GetNodes($n.Nodes)
}
}
$form = New-Object System.Windows.Forms.Form
$form.Text ="Test"
$form.Controls.AddRange(#(
($txt = [System.Windows.Forms.TextBox] #{
Location = [System.Drawing.Point]::new(8, 8);
Width = 100;
}),
($btn = [System.Windows.Forms.Button] #{
Location = [System.Drawing.Point]::new(120, 8);
Width = 50;
Text = "Search";
}),
($tree = [System.Windows.Forms.TreeView] #{
Location = [System.Drawing.Point]::new(8, 40);
Width = 170;
HideSelection = $false
})
))
$form.AcceptButton= $btn
$tree.Nodes.Add("A1", "A1")
$tree.Nodes.Add("A2", "A2")
$tree.Nodes[0].Nodes.Add("A11", "A11")
$tree.Nodes[0].Nodes.Add("A12", "A12")
$tree.Nodes[1].Nodes.Add("A21", "A21")
$tree.Nodes[1].Nodes.Add("A22", "A22")
$btn.Add_Click({param($sender,$e)
$nodes = GetNodes($tree.Nodes)
foreach ($node in $nodes) {
if($node.Text -like $txt.Text){
$tree.SelectedNode = $node
$node.EnsureVisible()
break
}
}
})
$form.ShowDialog() | Out-Null
$form.Dispose()
Assuming you are searching on a data source like a folder structure, this is what I'll do:
Create a function to get list of all directories recursively into a list
Create a function to filter the list of directories and return a list of directories which contain a specific text in their names.
Create a function to populate treeview
create a function to highlight treenode if it contains a specific text
Then in the text-changed event of the textbox, I'll filter and highlight tree:
Here is the code:
Add-Type -AssemblyName System.Windows.Forms
function GetPaths($root)
{
Get-ChildItem $root -Recurse -Directory | % {
$_.FullName.Replace($root, "").Trim("\")}
}
function FilterPaths($paths, $like)
{
$paths | ? {$_ -like "*$like*"} | % {
$i = $_.LastIndexOf("$like", [System.Globalization.CompareOptions]::IgnoreCase)
if($i -gt -1) {
$j = $_.IndexOf("\", $i, [System.Globalization.CompareOptions]::IgnoreCase)
if($j -gt -1) {
$_.SubString(0,$j)
} else {
$_
}
}
}
}
function GetNodes($nodes)
{
foreach ($n in $nodes) {
$n
GetNodes($n.Nodes)
}
}
function HighlightNodes($nodes, $like)
{
if(!$like){ return }
$nodes | ? {$_ -like "*$like*"} | % {
$_.BackColor = "Yellow"
}
}
function PopulateTree($treeView, $paths)
{
$treeView.Nodes.Clear()
foreach ($path in $paths)
{
$lastNode = $null
$subPathAgg = ""
foreach ($subPath in ($path -split '\\'))
{
$subPathAgg += ($subPath + '\')
$nodes = $treeView.Nodes.Find($subPathAgg, $true)
if ($nodes.Length -eq 0) {
if ($lastNode -eq $null) {
$lastNode = $treeView.Nodes.Add($subPathAgg, $subPath)
} else {
$lastNode = $lastNode.Nodes.Add($subPathAgg, $subPath)
}
} else {
$lastNode = $nodes[0]
}
}
}
}
$form = New-Object System.Windows.Forms.Form
$form.Text ="Test"
$form.Controls.AddRange(#(
($txt = [System.Windows.Forms.TextBox] #{
Location = [System.Drawing.Point]::new(8, 8);
Width = $form.ClientSize.Width - 16;
Anchor = [System.Windows.Forms.AnchorStyles]13
}),
($tree = [System.Windows.Forms.TreeView] #{
Location = [System.Drawing.Point]::new(8, 40);
Width = $form.ClientSize.Width - 16;
Anchor = [System.Windows.Forms.AnchorStyles]15
Height = 200;
HideSelection = $false
})
))
$form.AcceptButton= $btn
$root = "C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\ItemTemplates\CSharp"
$paths = GetPaths $root
PopulateTree $tree $paths
$tree.ExpandAll()
$txt.Add_TextChanged({param($sender,$e)
$tree.BeginUpdate()
$like = $txt.Text
$filtered = FilterPaths $paths $like
PopulateTree $tree $filtered
HighlightNodes (GetNodes $tree.Nodes) $like
$tree.ExpandAll()
$tree.TopNode = $tree.Nodes[0]
$tree.EndUpdate()
})
$form.ShowDialog() | Out-Null
$form.Dispose()

Create an object from an array

I have a string of data which I turn into an array thus:
$cfg_data = #(
"AppName1,data1",
"AppName2,data2"
)
I then run a foreach query to work on each line at a time:
FOREACH($a in $cfg_data)
{
$dataSplit = $a -split"(,)"
$AppN = $dataSplit[0]
$AppD = $dataSplit[2]
#Do stuff here
}
But I want to convert this from a string to an Object, so I can add/remove additional items,
and then run some more foreach statements, updating the different bits as I go.
I have got as far as:
FOREACH($a in $cfg_data)
{
$dataSplit = $a -split"(,)"
$AppN = $dataSplit[0]
$AppD = $dataSplit[2]
$objHere = #(
#{
appItem = "$AppN";
appData = "$AppD";
})
}
But when I check $objHere it just has the last entry in it (AppName2, datat2)
I tried adding:
$b=0
and
$objHere[$b]
but then I get
Array assignment failed because index '1' was out of range.
What is the correct way to do this?
By declaring $objHere in the loop, you overwrite the value on each iteration. You need to initialise an empty array outside the loop and append to it from within the loop:
$objHere = #()
foreach($a in $cfg_data)
{
$dataSplit = $a -split"(,)"
$AppN = $dataSplit[0]
$AppD = $dataSplit[2]
$objHere +=
#{
appItem = "$AppN";
appData = "$AppD";
}
}
In addition, you're not actually creating an object, you're creating a hashtable. If you wanted to create an object instead, you could do this:
$objHere = #()
foreach($a in $cfg_data)
{
$dataSplit = $a -split"(,)"
$AppN = $dataSplit[0]
$AppD = $dataSplit[2]
$objHere += New-Object PSObject -property #{appItem = "$AppN";appData = "$AppD";}
}
Giving:
appItem appData
------- -------
AppName1 data1
AppName2 data2

How to append to powershell Hashtable value?

I am interating through a list of Microsoft.SqlServer.Management.Smo.Server objects and adding them to a hashtable like so:
$instances = Get-Content -Path .\Instances.txt
$scripts = #{}
foreach ($i in $instances)
{
$instance = New-Object Microsoft.SqlServer.Management.Smo.Server $i
foreach($login in $instance.Logins)
{
$scripts.Add($instance.Name, $login.Script())
}
}
So far so good. What I want to do now is append a string to the end of the hashtable value. So for an $instance I want to append a string to the hashtable value for that $instance. How would I do that? I have started with this, but I'm not sure if I'm on the right track:
foreach ($db in $instance.Databases)
{
foreach ($luser in $db.Users)
{
if(!$luser.IsSystemObject)
{
$scripts.Set_Item ($instance, <what do I add in here?>)
}
}
}
Cheers
$h= #{}
$h.add("Test", "Item")
$h
Name Value
---- -----
Test Item
$h."Test" += " is changed"
$h
Name Value
---- -----
Test Item is changed
I would go with this code.
$instances = Get-Content -Path .\Instances.txt
$scripts = #{}
foreach ($i in $instances)
{
$instance = New-Object Microsoft.SqlServer.Management.Smo.Server $i
foreach($login in $instance.Logins)
{
$scripts[$instance.Name] = #($scripts[$instance.Name]) + $login.Script().ToString()
}
}
.
foreach ($db in $instance.Databases)
{
foreach ($luser in $db.Users)
{
if(!$luser.IsSystemObject)
{
$scripts[$instance] = #($scripts[$instance]) + $luser.Script().ToString()
}
}
}
The result will be a hash table with each instance as a key, and an array of strings where each string is the T-SQL script for a user.
The .Script() method returns a string collection. There's probably a more elegant way of doing it, but replacing
$scripts.Set_Item ($instance, <what do I add in here?>)
with
$val = $scripts[$instance]
$val.Add("text to add")
$scripts.Set_Item($instance, $val)
should work.
$test = #{}
$test.Hello = "Hello World"
Write-Host "message from $($test.Hello)"
$test.Hello += " Cosmonaut"
Write-Host "message from $($test.Hello)"

Resources