Count occurrences of something in an array inside a foreach loop - arrays

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!

Related

How do I access properties of nested arrays in powershell within foreach loops?

I'm trying to write a script that checks whether a group of registry keys are set individually and then either sets them or modifies them conditional on thier current state.
The keys that I want to check/set contain a mix of Strings and DWORDs
I want to loop through an array that contains the key I want to set paired with the value.
I've tried as a hashtable/splatting but the input to Get-ItemProperty fails because of the value parameter so I tried basic arrays instead with no luck.
They are all at the same registry path but I was attempting to do something similar to this:
$Path = "HKLM:\Software\path\to\keys"
$Properties = (
('key', value),
('key2', value2),
('key3', 'value3')
)
foreach ($item in $Properties){
$exist = Get-ItemProperty -Path $Path -name $item[0]
if ($exist) {
Set-ItemProperty -Path $Path -Name $item[0] -Value $item[1]
} else {
New-ItemProperty -Path $Path -Name $item[0] -Value $item[1]
}
}
But no matter what I've tried I cannot retrieve the individual elements of the inner arrays.
I realize I could probably do this long-form and just do it line by line rather than attempting to iterate through, but this is definitely a more elegant way, and would be a great template if I need to do something similar in the future.
Holy moly. Nothing breaks you out of a rut like posting the question to an online forum.
The .GetValue() method is what I needed, not the raw index number.
...So $Item.GetValue(0) for the key name and $Item.GetValue(1) for the value.
Feel silly answering my own question but hopefully it helps someone else!

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($_) }

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: Comparing a value between two arrays, and extracting a related value

so here is what I'm trying to accomplish.
I have a form for a new starter, New Starter Form.csv, that has the following headers and information:
firstname,lastname,teamname,startdate
Joe,Bloggs,Security Admin,01/01/18
I have a different csv called Team List.csv, that has the following headers and information:
teamlead,teamname,resgroup
A B,Marketing,RESMARKETING01G
C D,Product,RESPRODUCT01G
E F,Advertising,RESADVERTISING01G
G H,Security Admin,RESSECURITYADMIN01G
I want to import both CSV files into Powershell, run a comparisson that takes the team name from the New Starter Form, and checks if there are any matches in the Team List, and if so, add the relevant RES group to the new starter in AD.
Currently, I can import them, compare them, find a match, and find an index number for the record, but I'm struggling to the take this index number, and use it to get the relevant RES group. So far the code looks like this:
$teamlist = import-csv "\\location\Team List.csv"
$newstarter = import-csv "\\otherlocation\New Starter Form.csv"
[string]$teamname = Compare-Object -includeequal -excludedifferent -PassThru $newstarter.teamname $teamlist.teamname
$teamname
[array]::indexof($teamlist,$teamname)
And running that, provides us with this in the console, showing that we can indeed see the match, and that the matching record is the last (-1) one:
PS C:\WINDOWS\system32> $teamlist = import-csv "\\location\Team List.csv"
$newstarter = import-csv "\\otherlocation\New Starter Form.csv"
[string]$teamname = Compare-Object -includeequal -excludedifferent -PassThru $newstarter.teamname $teamlist.teamname
$teamname
[array]::indexof($teamlist,$teamname)
Security Administration
-1
I've not got a lot of experience with Powershell, and my coding knowledge is pretty limited overall, but I'm used to the concept that I can save the index value as a variable, and then I could call that variable back to do something like $teamlist.resgroup[VARIABLE HERE].
But if I try and declare a new variable before [array]::indexof($teamlist,$teamname), Powershell isn't happy.
Whilst I've not looked into it, I believe a possible alternative could be to add in a huge switch statement, but I may be looking at having 100+ teams overall, and I'd like to avoid inefficient code wherever I can. Am I missing something obvious though? Is there a better way (Or even just a functioning way would be great!) that this could work?
Any help you can provide would be greatly appreciated!
$teamlist = import-csv "\\location\Team List.csv"
$newstarter = import-csv "\\otherlocation\New Starter Form.csv"
# get a single new starter
$person = $newstarter | Where-Object { $_.firstname -eq 'Joe' -and $_.lastname -eq 'Bloggs' }
# get the new starters team
$team = $teamlist | Where-Object { $_.teamname -eq $person.teamname }
# get the new starters resource group
$resgroup = $team.resgroup
# use the resource group - this simply writes it to the console
Write-Host $resgroup
The code above will:
import your two csvs
grab a single new starter from your new starter csv, based on first &
last name
grab the team & resource group for that new starter from the team list
display the resgroup (this is where you will need to use to populate AD)

ACL "fuzzy" comparision

I'm trying to compare ACLs on a folder with a reference set of ACLs, and then list any exceptions. The "fuzzy" part of the equation is that I want to be able to disregard any unknown SID. So creating a reference folder with the perms I want to test won't work to use Compare-Object between it and my test folder.
The underlying scenario is that I am cleaning up old user directories where the actual user account has been deleted (this is where the non-resolved SID comes in). By default, the folders include perms for Administrator and the like, which I don't care about. There are some folders, however, where another user has been granted explicit permissions, and I want to capture these. Unfortunately, there aren't any shortcuts I can use to check: e.g. -IsInherited or the like to exclude ACLs I don't care about.
Per the below, I can dump the ACLs out into an array
$acl = get-acl f:\user_folder
$access = $acl.Access | ForEach-Object { $_.identityReference.value }
$access
BUILTIN\Administrators
MYDOMAIN\JBLOGGS
S-1-5-21-4444444444-9999999-1111111111-74390
MYDOMAIN\Domain_Group ###Yes, the group has an underscore in the name
I can create another array of the users I want to ignore, including a partial string to match any unresolved SID.
$defaults = #("BUILTIN\Administrators","MYDOMAIN\DomainGroup","S-1-5-21")
So how do I compare my $defaults array with the $access array and output only the exceptions like "MYDOMAIN\JBLOGGS"?
I'm trying a foreach, but I'm stumped about grabbing that exception. The following still outputs the SID I want to avoid. I'm hoping to also avoid too many nested "IFs".
$access | ForEach { If ($defaults -notcontains $_) { Write-Output $_ } }
MYDOMAIN\JBLOGGS
S-1-5-21-4444444444-9999999-1111111111-74390 #Do not want!
If I put the wildcard $_* into the -notcontains, I get the whole contents of $access again.
I'd do something like this:
$defaults = 'BUILTIN\Administrators', 'MYDOMAIN\DomainGroup', 'S-1-5-21*'
$acl.Access | Where-Object {
$id = $_.IdentityReference
-not ($defaults | Where-Object { $_ -like $id })
} | Select-Object -Expand value
$defaults | Where-Object { $_ -like $id } does a wildcard match of the given identity against all items of $defaults. The wildcard * at the end of S-1-5-21* allows to match all strings starting with S-1-5-21. The negation -not inverts the result so that only identities not having a match in $defaults pass the filter.
give the users you want to ignore some right on a dummy folder, get the acl of that folder and then compare whith the acl of your actual folder
$genericACL = get-acl c:\temp\dummy
$folderacl = get-acl f:\user_folder
$exceptions= $folderacl.Access.identityreference.value |?{ ($_ -notin $genericACL.access.identityreference.value) -and ($_.strartswith('S-1-5-21') -eq $false)) }
In the end, it was fairly simple, thanks to the help above.
I managed to omit the fact in the original question where I required it to work in Powershell v2.
$defaults = #("BUILTIN\Administrators","MYDOMAIN\DomainGroup")
$acl = get-acl $folder
$access = $acl.Access | ForEach-Object { $_.identityReference.value }
# check that no other account still has access to the folder
$access | ForEach {
If ($defaultACL -notcontains $_ -and $_ -notlike 'S-1-5-21*') {
write-output "Extra perms:$user $_"
}

Resources