Powershell array of arrays loop process - powershell

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.

Related

Powershell Array Output to html

Apologies if this is irrelevant but I'm new to powershell and I've been scratching my head on this for a few days on and off now. I'm trying to write a script that will output two columns of data to a html document. I've achieved most of it by learning through forums and testing different combinations.
The problem is although it gives me the result I need within powershell itself; it will not properly display the second column results for Net Log Level.
So the script looks at some folders and pulls the * value which is always three digits (this is the Site array). It then looks within each of these folders to the Output folder and grabs a Net Log Level node from a file inside there. The script is correctly listing the Sites but is only showing the last value for Net Log Level which is 2. You can see this in the screenshot above. I need this to take every value for each Site and display as appropriate. The image of the incorrect result is below. I need the result to be 1,4,2,2,2. Any help would be greatly appreciated!
function getSite {
Get-ChildItem C:\Scripts\ServiceInstalls\*\Output\'Config.exe.config' | foreach {
$Site = $_.fullname.substring(27, 3)
[xml]$xmlRead = Get-Content $_
$NetLogLevel = $xmlRead.SelectSingleNode("//add[#key='Net Log Level']")
$NetLogLevel = $NetLogLevel.value
New-Object -TypeName System.Collections.ArrayList
$List1 += #([System.Collections.ArrayList]#($Site))
New-Object -TypeName System.Collections.ArrayList
$List2 += #([System.Collections.ArrayList]#($NetLogLevel))
}
$Results = #()
ForEach($Site in $List1){
$Results += [pscustomobject]#{
"Site ID" = $Site
"Net Log Level" = $NetLogLevel
}
}
$Results | ConvertTo-HTML -Property 'Site','Net Log Level' | Set-Content Output.html
Invoke-Item "Output.html"
}
getSite
Restructure your code as follows:
Get-ChildItem 'C:\Scripts\ServiceInstalls\*\Output\Config.exe.config' |
ForEach-Object {
$site = $_.fullname.substring(27, 3)
[xml]$xmlRead = Get-Content -Raw $_.FullName
$netLogLevel = $xmlRead.SelectSingleNode("//add[#key='Net Log Level']").InnerText
# Construct *and output* a custom object for the file at hand.
[pscustomobject] #{
'Site ID' = $site
'Net Log Level' = $netLogLevel
}
} | # Pipe the stream of custom objects directly to ConvertTo-Html
ConvertTo-Html | # No need to specify -Property if you want to use all properties.
Set-Content Output.html
As for what you tried:
New-Object -TypeName System.Collections.ArrayList in effect does nothing: it creates an array-list instance but doesn't save it in a variable, causing it to be enumerated to the pipeline, and since there is nothing to enumerate, nothing happens.
There is no point in wrapping a [System.Collections.ArrayList] instance in #(...): its elements are enumerated and then collected in a regular [object[]] array - just use #(...) by itself.
Using += to "grow" an array is quite inefficient, because a new array must be allocated behind the scenes every time; often there is no need to explicitly create an array - e.g. if you can simply stream objects to another command via the pipeline, as shown above, or you can let PowerShell itself implicitly create an array for you by assigning the result of a pipeline or foreach loop as a whole to a variable - see this answer.
Also note that when you use +=, the result is invariably a regular [object[] array, even if the RHS is a different collection type such as ArrayList.
There are still cases where iteratively creating an array-like collection is necessary, but you then need to use the .Add() method of such a collection type in order to grow the collection efficiently - see this answer.
Instead of populating two separate lists, simply create the resulting objects in the first loop:
function getSite {
$Results = Get-ChildItem C:\Scripts\ServiceInstalls\*\Output\'Config.exe.config' | ForEach-Object {
$Site = $_.fullname.substring(27, 3)
[xml]$xmlRead = Get-Content $_
$NetLogLevel = $xmlRead.SelectSingleNode("//add[#key='Net Log Level']")
$NetLogLevel = $NetLogLevel.value
[pscustomobject]#{
"Site ID" = $Site
"Net Log Level" = $NetLogLevel
}
}
$Results | ConvertTo-HTML -Property 'Site', 'Net Log Level' | Set-Content Output.html
Invoke-Item "Output.html"
}
getSite

Alphanumerical sorting not working in Array

We have a directory, which features many subdirectories (one per day) with serveral files in it. Unfortunately, files can be resent - so a file of 2020-01-01 can be resend (with slightly different filename, since a timestamp is added to the filename) on 2020-02-03. The structure looks something like this:
TopDir
20200801
AFile_20200801_20200801150000 (Timestamped 2020-08-01 15:00:00)
BFile_20200801_20200801150000
CFile_20200801_20200801150000
20200802
AFile_20200802_20200801150000
BFile_20200802_20200801150000
CFile_20200802_20200801150000
AFile_20200801_20200802150000 (Timestamped 2020-08-02 15:00:00)
So the AFile of 2020-08-01 has been resent on 2020-08-02 at 3 PM.
I am now trying to retrieve a list with the most recent file per day, so I built up an array and populated it with all files below TopDir (recurively). So far so good, all files are found:
$path = "Y:\";
$FileArray = #()
$FileNameArray = #()
$FileArrayCounter = 0
foreach ($item in Get-ChildItem $path -Recurse)
{
if ($item.Extension -ne "")
{
$StringPart1, $StringPart2, $StringPart3, $StringPart4 = $item.Name.Split('_');
$FileNameShort = "{0}_{1}_{2}" -f $StringPart1.Trim(), $StringPart2.Trim(), $StringPart3.Trim();
$FileNameShort = $FileNameShort.Trim().ToUpper();
$FileArray += #{FileID = $FileArrayCounter; FileNameShort = $FileNameShort; FileName = $item.Name; FullName = $item.FullName; LastWriteTime = $item.LastWriteTime};
$FileArrayCounter ++;
}
}
$FileArray = $FileArray | sort FileNameShort; ##{Expression={"FileNameShort"}; Ascending=$True} #, #{Expression={"LastWriteTime"}; Descending=$True}
foreach($f in $FileArray)
{
Write-host($f.FileNameShort, $f.LastWriteTime)
}
Write-host($FileArrayCounter.ToString() + " Dateien gefunden");
The newly added column "FileNameShort" includes a substring of the filename. With this done, I receive two Rows for AFile_20200801:
AFile_20200801, AFile_20200801_20200801150000, ...
AFile_20200801, AFile_20200801_20200802150000, ...
However, when I try to sort my array (see above code), the output is NOT sorted by name. Instead I receive something like the following:
AFile_20200801
CFile_20200802
AFile_20200801
BFile_20200801
What I want to achieve is a sorting by FileNameShort ASCENDING and LastWriteTime DESCENDING.
What am I missing here?
Your sort does not work because $FileArray is an array of hash tables. The syntax Sort FileNameShort is binding the FileNameShort property to the -Property parameter of Sort-Object. However, the hash table does not contain a property called FileShortName. You can see this if you run $FileArray[0] | Get-Member.
If you create them as custom objects, the simple sort syntax works.
$FileArray += [pscustomobject]#{FileID = $FileArrayCounter; FileNameShort = $FileNameShort; FileName = $item.Name; FullName = $item.FullName; LastWriteTime = $item.LastWriteTime}
$FileArray | Sort FileNameShort # This will sort correctly
As an aside, I do not recommend using += to seemingly add elements to an array. It is best to either output the results inside of your loop and save the loop results or create a list with an .Add() method. The problem with += is the current array is expanded into memory and those contents are then used to create a new array with the new items. As the array grows, it becomes increasingly non-performant. See below for a more efficient example.
$FileArray = foreach ($item in Get-ChildItem $path -Recurse)
{
if ($item.Extension -ne "")
{
$StringPart1, $StringPart2, $StringPart3, $StringPart4 = $item.Name.Split('_');
$FileNameShort = "{0}_{1}_{2}" -f $StringPart1.Trim(), $StringPart2.Trim(), $StringPart3.Trim();
$FileNameShort = $FileNameShort.Trim().ToUpper();
# Outputting custom object here
[pscustomobject]#{FileID = $FileArrayCounter; FileNameShort = $FileNameShort; FileName = $item.Name; FullName = $item.FullName; LastWriteTime = $item.LastWriteTime};
$FileArrayCounter ++;
}
}
I just found the solution:
$FileArray = $FileArray | sort #{Expression={[string]$_.FileNameShort}; Ascending=$True}, #{Expression={[datetime]$_.LastWriteTime}; Descending=$True}
Still I don't know, why the first sorting did not work as expected.

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 compare arrays and get unique values

I am currently trying to write a powershell script that can be run weekly on two CSV files, to check they both contain the same information. I want the script to output anything that appears in one file but not the other to a new file.
The script I have written so far compares the two but only adds <= and => to the values.
It also doesn't work all the time, because I manually checked the file and found results that existed in both.
Code below:
$NotPresents = compare-object -ReferenceObject $whatsup -DifferenceObject $vmservers -Property device
foreach ($NotPresent in $NotPresents)
{
Write-Host $NotPresent.device
}
$NotPresents | Out-File Filepath.txt
$NotPresents.count
Any ideas what I have done wrong?
In order to avoid having to iterate over one of the arrays more than once*, you may want to throw them each into a hashtable:
$whatsupTable = #{}
foreach($entry in $whatsup){
$whatsupTable[$entry.device] = $true
}
$vmserversTable = #{}
foreach($entry in $vmservers){
$vmserversTable[$entry.device] = $true
}
Now you can easily find the disjunction with a single loop and a lookup against the other table:
$NotInWhatsUp = $vmservers |Where { -not $whatsupTable[$_] }
$NotInVMServers = $whatsup |Where { -not $vmserversTable[$_] }
*) ok, technically we're looping through each twice, but still much better than nested looping

How to export mixed type objects to csv file?

I'm writing a script that returns a list of objects that most of them have different number of properties. When I print it in the console everything is OK, but when I try to export to CSV only those fields that are common in all objects get exported. All others are cropped.
I use the Add-Member cmdlet to add more properties but not all of the objects get the same number of properties.
For example I try to export 2 objects where one is like this:
FirstObject:{
Network0:nic1,
Network1:nic2,
Network2:nic3,
Network3:nic4,
Name:VirtualMachine1
}
SecondObject:{
Network0:nic1,
Network1:nic2,
Name:VirtualMachine1
}
The Network property is added with Add-Member cmdlet. The problem I get when exporting to CSV is that Network2 and Network3 properties from the first object are cropped and all the columns I get is Network0, Network1, and Name.
What I would like to know is there a way to export all the properties and if one of the objects doesn't have the property, just assign $null?
P.S. I have a solution just to add those fields manually with a loop, but I was wondering maybe there is a cleaner solution built in PowerShell which I missed?
Update:
I found out that it provides the same columns to the file that are in the first object. All other fields are ignored. So to be more exact I need all columns in all objects. If some objects do not have the field, then it should be printed empty.
Just a few lines of code that add missing properties.
#sample setup
$one = [pscustomobject]#{
Network0='nic1'
Network1='nic2'
Network2='nic3'
Network3='nic4'
Name='VirtualMachine1'
}
$two = [pscustomobject]#{
Network0='nic1'
Network1='nic2'
Name='VirtualMachine2'
}
$three = [pscustomobject]#{
Network0='nic1'
Name='VirtualMachine3'
}
$export = ($one,$two,$three)
#build list of all properties available to $allProps
$export | % -begin { $allProps = #() } -process { $allProps = [linq.enumerable]::union([object[]](($_.psobject.Properties).Name), [object[]]$allProps) }
#convert each object in $export to new custom object having all properties and put to $result
$export | % -begin { $result = #() } -process { $__ = $_; $o = #{ }; $allProps | %{ $o += #{ $_ = $__.$_ } }; $result+=[pscustomobject]$o }
#export $result to csv
$result | Export-Csv $env:TEMP\export.csv -NoTypeInformation -Force
Get-Content $env:TEMP\export.csv
"Network1", "Network3", "Network0", "Name", "Network2"
"nic2", "nic4", "nic1", "VirtualMachine1", "nic3"
"nic2",, "nic1", "VirtualMachine2",
,, "nic1", "VirtualMachine3",
>> Script Ended
Things to note:
[linq.enumerable]::union is used to easy build list of all available properties across all objects.
($_.psobject.Properties).Name is shortcut to #($_.psobject.Properties | select -ExpandProperty Name), it contains array of property names
$__ = $_ is a trick for nested loop
$o += #{ $_ = $__.$_ } adds key-value pairs to output object; trick here is that even if property $_='nic4' does not exists in $__ export object, powershell does not throw error and returns $null. Note that this will not work when Set-StrictMode is set -Version 2 or later.