Split an Array of Objects to Multiple objects - arrays

I wanted to split the output of the following command:
PS C:\Windows\system32> $array = get-vm | Select-Object
#{Name="VMName";Expression=$_.name}},
#{Name='VirtualNetwork';e={ $_.VirtualNetworkAdapters | Foreach-Object{
$_.VirtualNetwork}}},
#{Name='PhysicalMACAddress';e={$_.VirtualNetworkAdapters| Foreach-Object{
$_.physicaladdress}}}
whose output is like
to an output like this, can some one please help.

In my opinion you're in the wrong direction. You'd have to get all objects first, check for the one with most virtual networks, and create objects with a lot of VirtualNetworkN properties.

Related

Possible to have different array names when write-output PSCustomObject]#{ in my function?

Is there any way to name the variable for an array something in my Catch { and then have another array name for the array inside Try/script part of my function?
Cause when i try doing like this $computerObject = [PSCustomObject]#{
and then doing Write-Output $computerArray i can only get either my variables inside Try/script array being displayed inside Powershell window. Or only get the $error message from my Catch.. Is there any way to name each array something so i can do like below.
write-host "Results"
Write-Output $computerArray - display my first array here
write-host "Failed: computerlist" -foregroundcolor red
Write-Output $computerArray2 - display $error computers here. $error should just include computers who did not answer to ping and other stuff from my invoke-command computerlist.txt
The only true answer to why i need this separately is that sometimes i want my array in a CSV file. And sometimes i just want to copy info directly from Powershell window. And then its more practical to have failed computers separated and not in the same array output.
This function (as mentioned in comments) doesn't leverage the CIM cmdlets parallel capabilities, would recommend some tweaks to it but to answer the actual question, how can you "split" the output between success and fail:
The function as-is, doesn't require any modification to achieve this, it's try and catch blocks are outputting objects with the same properties and luckily one of those properties is Error and it's value is a boolean so you can simply first query all the computers and then split the result using .Where with Split mode.
The code would be like this:
$computers = 'computer1', 'computer2', ....
$computerArray = foreach($computer in $computers) {
Get-ComputerInformation -ComputerName $computer
}
# now we can split between FAIL and SUCCESS
$fail, $success = $computerArray.Where({ $_.Error }, 'Split')
$success | Export-Csv path\to\success.csv -NoTypeInformation
$fail | Export-Csv path\to\fail.csv -NoTypeInformation

Why can't I store the return value of a cmdlet [CimInstance] inside an arraylist?

I'm currently working on a script where i retrieve the following data on a host:
List of installed printers
List of installed printer drivers
List of used printer drivers
At the moment i simply achieve it this way:
$installedPrinters = Get-Printer
$installedDrivers = Get-PrinterDriver | Sort-Object -Property Name
$usedDrivers = $printerList | Sort-Object -Property DriverName | Select-Object DriverName | Get-Unique -AsString
For convenience reasons i'm now trying to use an arraylist instead of 3 different variables for storing this data but i somehow don't seem to get this to work.
As soon as i try something like that ...
$data.Add({Get-Printer})
or
Get-Printer | $data.Add($_)
... i get either a bunch of errors or simply the value 'Get-Printers' as string stored in the arraylist.
Weirdly enough it seems to work if i first store the returned data from the Get-Printer cmdlet inside a dedicated variable and then add this variable to the arraylist.
Can somebody please help me get my head around this? As of now this behaviour doesn't really seem to make any sense to me.
Your syntax is flawed.
Use one of the following (assuming that $data contains a System.Collections.ArrayList instance):
$data.AddRange((Get-Printer))
Or, less efficiently:
Get-Printer | ForEach-Object { $null = $data.Add($_) }

Count occurrences of something in an array inside a foreach loop

I have a product CSV file that I have imported into $products
If something occurs more than once with the same name I want to populate the ParentSKU field, otherwise leave it blank.
Excuse the pseudocode but I'm imagining something like this:
foreach ($item in $products) {
if ($item.name.count -gt 1) {
$item.ParentSKU = $item.name }
else { } # do nothing
}
$item.name.count isn't correct but I hope my thinking is on the right track?
Many thanks for any advice
Powershell Object lists aren't smart enough to know that there's multiple of any one item, so you're going to have to iterate through (manually or otherwise) to find whether there's multiples here.
Since you're going to be making modifications to any duplicates, it may make sense to loop through and find duplicates manually, but it doesn't really follow the "powershell" philosophy / approach.
If you want to use powershell's built-in & powerful piping features, you might try a solution like this, which would grab all the PSObjects with duplicates using Where-Object, then sets the values for all those PSObjects.
$products |
Group-Object -Property Name |
Where-Object -FilterScript {
$_.Count -gt 1
} |
Select-Object -ExpandProperty Group |
Foreach-Object { $_.ParentSKU = $_.Name }
Since everything is passed by reference, your $products object will have the modified values!

Sort Hashtable with Arrays as values

Description: I'm building a PowerShell-script that searches for files, then gives them unique names, copies them and then verifies them via hash-calculation - I chose to split the script in functions for each step, so it's easier to maintain the whole thing.
To get all values from one function to the other, I chose to use [hashtable]$FooBar - inside $FooBar, there are multiple arrays, such as FullName or OutputPath (which may change per file as they will be copied to subfolders named yyyy-mm-dd). All arrays are correlating with each other (meaning that index 1 contains all values of the first file, index 2 the values for the second file,...) and this works fine as of now.
A short simplified visualisation:
$FooBar = #{}
$FooBar.FullName = #()
$FooBar.Size = #()
$FooBar.Ext = #()
Get-ChildItem | ForEach-Object {
$FooBar.FullName += $_.FullName
$FooBar.Size += $_.Length
$FooBar.Ext += $_.Extension
}
However, I now need to sort them all by one value-set of one of the arrays, e.g. the size. Or, visualised again:
# From:
$FooBar
Name Value
---- -----
fullname {D:\AAA.XYZ, D:\BBB.ZYX, D:\CCC.YZX}
size {222, 111, 555}
extension {.XYZ, .ZYX, .YZX}
# To:
$FooBar = $FooBar | Sort-Object -Property Size -Descending
$FooBar
Name Value
---- -----
fullname {D:\CCC.YZX, D:\AAA.XYZ, D:\BBB.ZYX}
size {555, 222, 111}
extension {.YZX, .XYZ, .ZYX}
I tried $FooBar.GetEnumerator() | Sort-Object -Property Size, but this does not change anything. Google turned up suggestions on how to sort an array of hashtables, but in my case, it's the other way round, and I can't get my head around this because I don't even understand why this is a problem in the first place.
So my question is: is there any way to sort all arrays inside the hashtable by the value-set of one of the arrays? I can't get my head around this.
Disclaimer: I'm a PowerShell-autodidact with no reasonable background in scripting/programming, so it might well be that my "include everything in one hashtable"-solution isn't going to work at all or might be extremely inefficient - if so, please tell me.
The easiest way to go about what I believe you are trying to do is Select-Object
$fooBar = Get-ChildItem | Select-Object FullName, Size, Extension
This will create an array of new objects that only have the desired properties. The reason this works and your method doesn't is because Sort-Object works on properties and the property you are specifying is behind a few layers.
If you need more flexibility than just exact properties, you can create your own like this
$fooBar = Get-ChildItem | Select-Object #{Name = 'SizeMB'; Expression = {$_.Size / 1MB}}
Or manually create new properties with the [PSCustomObject] type accelerator:
$fooBar = Get-ChildItem | ForEach-Object {
[PSCustomObject]#{
FullName = $_.FullName
Extension = $_.Extension
Size = $_.Size
}
}
Update
If you need to add additional properties to the object after it's initially created you have a few options.
Add-Member
The most common method by far is by using the Add-Member cmdlet.
$object | Add-Member -MemberType NoteProperty -Name NewProperty -Value 'MyValue'
$object
Something important to keep in mind is that by default this cmdlet does not return anything. So if you place the above statement at the end of a function and do not separately return the object, your function won't return anything. Make sure you either use the -PassThru parameter (this is also useful for chaining Add-Member commands) or call the variable afterwards (like the example above)
Select-Object
You can select all previous properties when using calculated properties to add members. Keep in mind, because of how Select-Object works, all methods from the source object will not be carried over.
$fooBar | Select-Object *, #{Name = 'NewProperty'; Expression = {'MyValue'}}
psobject.Properties
This one is my personal favorite, but it's restricted to later versions of PowerShell and I haven't actually seen it used by anyone else yet.
$fooBar.psobject.Properties.Add([psnoteproperty]::new('NewProperty', 'MyValue'))
$fooBar
Each member type has it's own constructor. You can also add methods to $fooBar.psobject.Methods or either type to $fooBar.psobject.Members. I like this method because it feels more explicit, and something about adding members with members feels right.
Summary
The method you choose is mostly preference. I would recommend Add-Member if possible because it's the most used, therefore has better readability and more people who can answer questions about it.
I would also like to mention that it's usually best to avoid adding additional members if at all possible. A function's return value should ideally have a reliable form. If someone is using your function and they have to guess when a property or method will exist on your object it becomes very difficult to debug. Obviously this isn't a hard and fast rule, but if you need to add a member you should at least consider if it would be better to refactor instead.
For all practical purposes I'd strongly suggest you just store the objects you need in a single array, sort that once and then reference the individual properties of each object when needed:
$FooBar = Get-ChildItem |Sort-Object -Property Length
# Need the Extension property of the object at index 4?
$FooBar[4].Extension
To answer your actual question:
Array.Sort() has an overload that takes keys and values arrays separately. You could make a copy of the array you want to sort on for each other property you want to sort:
# Create hashtable of correlated arrays
$FooBar = #{}
$FooBar.FullName = #()
$FooBar.Size = #()
$FooBar.Ext = #()
# Types cast explicitly to avoid Array.Sort() calling .CompareTo() on the boxing object
Get-ChildItem | ForEach-Object {
$FooBar.FullName += [string]$_.FullName
$FooBar.Size += [int]$_.Length
$FooBar.Ext += [string]$_.Extension
}
# Define name of reference array property
$SortKey = 'Size'
# Sort all arrays except for the reference array
$FooBar.Keys |Where-Object {$_ -ne $SortKey} |ForEach-Object {
# Copy reference values to new array
$Keys = $FooBar[$SortKey].Clone()
# Sort values in target array based on reference values
[array]::Sort($Keys,$FooBar[$_])
}
# Finally sort the reference array
[array]::Sort($FooBar[$SortOn])
The above only works as long as the reference array is made up of value types
PowerShell makes working with objects ridiculously easy.
Try:
$FooBar = Get-Childitem
$FooBar | Get-Member
This will tell you that $Foobar actually contains objects of FileInfo and DirectoryInfo type, and show you the Properties available.
$FooBarSortedBySizeDesc = $FooBar | Sort-Object Length -Descending
$FooBarFullNamesOnly = $FooBar.FullName

Powershell - compare Active Directory usernames with e-mail address

I am currently trying to find all AD users that have been created using the model "firstname#domain.com" versus our new standard of "FirstInitialLastName#domain.com". I'm using the Quest ActiveRoles modules, specifically Get-QADUser to pull down my user details:
Get-QADUser -enabled -IncludedProperties PrimarySMTPAddress | ?{$_.Type -match "User"} | Select-Object FirstName,PrimarySMTPAddress ...
That gets me a list of user first names and their SMTP address. Where I am stumped is how to compare the results.
I thought normalizing the values (either adding "#domain.com" to the first name string or stripping "#domain.com" from the SMTP string) and then doing a -ieq test would be the best approach. I have found I can do the first with:
%{ $address=$($_.FirstName + "#domain.com";) }
But I can't figure out how to then test $address against the PrimarySMTPAddress string. I can create a second variable with:
%{ $smtp=$($_.PrimarySMTPAddress); }
and get the result:
[PS] C:\>$addy -ieq $smtp
True
I'm just unclear how to do it all in stream so that I can process my tree at once. If this is something that's just more suited to a script than a single line, that's fine too. Coming from the glorious world of BASH my brain just wanted to one-line it.
Get-QADUser -Enabled -Email * -SizeLimit 0 |
Where-Object {$_.Email.Split('#')[0] -eq $_.FirstName }
Try this:
Get-QADUser -enable -IncludedProperties PrimarySMTPAddress |
? { $_.PrimarySMTPAddress -match ("^"+[regex]::escape("$($_.firstname)")+"#domain.com") }

Resources