How to dynamically add PSCustomObjects to a list [duplicate] - powershell

This question already has answers here:
Add items into a collection array dynamically
(2 answers)
Closed 1 year ago.
I am creating a script to parse a CSV file, where I store the content of each indexed field in the CSV as a NoteProperty in a PSCustomObject.
As I parse the file line by line, I add the PSCustomObject to a list type. When I output my list, I want to be able to do something like:
$list | Format-Table
and have a nice view of each row in the csv file, separated into columns with the heading up top.
Problem
When I add a PSCustomObject to the list, it changes the type of the list to a PSCustomObject. In practice, this has the apparent effect of applying any updates made to that PSCustomObject to every element in the list retroactively.
Here is a sample:
$list = [System.Collections.Generic.List[object]]::new()
$PSCustomObject = [PSCustomObject]#{ count = 0}
Foreach ($i in 1..5) {
$PSCustomObject.count +=1
$list.Add($PSCustomObject)
}
Expected Output:
PS>$list
count
-----
1
2
3
4
5
Actual Output:
PS>$list
count
-----
5
5
5
5
5
Question
Is there any way to get the expected output?
Limitations / additional context if it helps
I'm trying to optimize performance, as I may parse very large CSV files. This is why I am stuck with a list. I understand the Add method in lists is faster than recreating an array with += for every row. I am also using a runspace pool to parse each field separately and update the object via $list.$field[$lineNumber] = <field value>, so this is why I need a way to dynamically update the PSCustomObject. A larger view of my code is:
$out = [hashtable]::Synchronized(#{})
$out.Add($key, #{'dataSets' = [List[object]]::new() } ) ### $key is the file name as I loop through each csv in a directory.
$rowTemplate = [PSCustomObject]#{rowNum = 0}
### Additional steps to prepare the $out dictionary and some other variables
...
...
try {
### Skip lines prior to the line with the headers
$fileParser = [System.IO.StreamReader]$path
Foreach ( $i in 1..$headerLineNumber ) {
[void]$fileParser.ReadLine()
}
### Load the file into a variable, and add empty PSCustomObjects for each line as a placeholder.
while ($null -ne ($line = $fileParser.ReadLine())) {
[void]$fileContents.Add($line)
$rowTemplate.RowNum += 1
[void]$out.$key.dataSets.Add($rowTemplate)
}
}
finally {$fileParser.close(); $fileParser.dispose()}
### Prepare the script block for each runspace
$runspaceScript = {
Param( $fileContents, $column, $columnIndex, $delimiter, $key, $out )
$columnValues = [System.Collections.ArrayList]::new()
$linecount = 0
Foreach ( $line in $fileContents) {
$entry = $line.split($delimiter)[$columnIndex]
$out.$key.dataSets[$linecount].$column = $entry
$linecount += 1
}
}
### Instantiate the runspace pool.
PS Version (5.1.19041)

You're (re-)adding the same object to the list, over and over.
You need to create a new object every time your loop runs, but you can still "template" the objects - just use a hashtable/dictionary instead of a custom object:
# this hashtable will be our object "template"
$scaffold = #{ Count = 0}
foreach($i in 1..5){
$scaffold.Count += 1
$newObject = [pscustomobject]$scaffold
$list.Add($newObject)
}
As mklement0 suggests, if you're templating objects with multiple properties you might want to consider using an ordered dictionary to retain the order of the properties:
# this hashtable will be our object "template"
$scaffold = [ordered]#{ ID = 0; Count = 0}
foreach($i in 1..5){
$scaffold['ID'] = Get-Random
$scaffold['Count'] = $i
$newObject = [pscustomobject]$scaffold
$list.Add($newObject)
}

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.

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
}

Foreach -parallel object

Recently we started working on scripts that take a very long time to complete. So we dug into PowerShell workflows. After reading some documentation I understand the basics. However, I can't seem to find a way to create a [PSCustomObject] for each individual item within a foreach -parallel statement.
Some code to explain:
Workflow Test-Fruit {
foreach -parallel ($I in (0..1)) {
# Create a custom hashtable for this specific object
$Result = [Ordered]#{
Name = $I
Taste = 'Good'
Price = 'Cheap'
}
Parallel {
Sequence {
# Add a custom entry to the hashtable
$Result += #{'Color' = 'Green'}
}
Sequence {
# Add a custom entry to the hashtable
$Result += #{'Fruit' = 'Kiwi'}
}
}
# Generate a PSCustomObject to work with later on
[PSCustomObject]$Result
}
}
Test-Fruit
The part where it goes wrong is in adding a value to the $Result hashtable from within the Sequence block. Even when trying the following, it still fails:
$WORKFLOW:Result += #{'Fruit' = 'Kiwi'}
Okay here you go, tried and tested:
Workflow Test-Fruit {
foreach -parallel ($I in (0..1)) {
# Create a custom hashtable for this specific object
$WORKFLOW:Result = [Ordered]#{
Name = $I
Taste = 'Good'
Price = 'Cheap'
}
Parallel {
Sequence {
# Add a custom entry to the hashtable
$WORKFLOW:Result += #{'Color' = 'Green'}
}
Sequence {
# Add a custom entry to the hashtable
$WORKFLOW:Result += #{'Fruit' = 'Kiwi'}
}
}
# Generate a PSCustomObject to work with later on
[PSCustomObject]$WORKFLOW:Result
}
}
Test-Fruit
You're supposed to define it as $WORKFLOW:var and repeat that use throughout the workflow to access the scope.
You could assign $Result to the output of the Parallel block and add the other properties afterwards :
$Result = Parallel {
Sequence {
# Add a custom entry to the hashtable
[Ordered]#{'Color' = 'Green'}
}
Sequence {
# Add a custom entry to the hashtable
[Ordered] #{'Fruit' = 'Kiwi'}
}
}
# Generate a PSCustomObject to work with later on
$Result += [Ordered]#{
Name = $I
Taste = 'Good'
Price = 'Cheap'
}
# Generate a PSCustomObject to work with later on
[PSCustomObject]$Result

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.

Creating dynamic variable array names and then adding object to them

What I'm trying to do is create array variable names dynamically, and then with a loop, add the object to its relevant array based on the hash table value being equal to the counter variable.
$hshSite = #{} # Values like this CO,1 NE,2 IA,3
$counter = $hshSite.count
For($i = $counter; $i -gt 0; $i--) {
New-Variable -Name "arr$i" -Value #()
}
If $counter = 3, I would create arrays $arr1, $arr2, $arr3
$csv = Import-CSV....
ForEach ($x in $csv) {
#if $hshSite.Name = $x.location (ie CO), look up hash value (1),
and add the object to $arr1. If $hshSite.Name = NE, add to $arr2
I tried creating the dynamic arrays with New-Variable, but having issues trying to add to those arrays. Is it possible to concatenate 2 variables names into a single variable name? So taking $arr + $i to form $arr1 and $arr2 and $arr3, and then I can essentially just do $arr0 += $_
The end goal is to group things based on CO, NE, IA for further sorting/grouping/processing. And I'm open to other ideas of getting this accomplished. Thanks for your help!
Just make your hash table values the arrays, and accumulate the values to them directly:
$Sites = 'CO','NE','IA'
$hshSite = #{}
Foreach ($Site in $Sites){$hshSite[$Site] = #()}
ForEach ($x in $csv)
{
$hshSite[$x.location] += <whatever it is your adding>
}
If there's a lot of entries in the csv, you might consider creating those values as arraylists instead of arrays.
$Sites = 'CO','NE','IA'
$hshSite = #{}
Foreach ($Site in $Sites){ $hshSite[$Site] = New-Object Collections.Arraylist }
ForEach ($x in $csv)
{
$hshSite[$x.location].add('<whatever it is your adding>') > $nul
}
You could quite easily do add items to a dynamically named array variable using the Get-Variable cmdlet. Similar to the following:
$MyArrayVariable123 = #()
$VariableNamePrefix = "MyArrayVariable"
$VariableNameNumber = "123"
$DynamicallyRetrievedVariable = Get-Variable -Name ($VariableNamePrefix + $VariableNameNumber)
$DynamicallyRetrievedVariable.Value += "added item"
After running the above code the $MyArrayVariable123 variable would be an array holding the single string added item.