Making multiple ForEach-Object more efficient - powershell

$prcs = (Get-Process).MainModule.FileName|%{Get-AuthenticodeSignature$_}|Select Path,Status
$prcs|?{$_.status -eq "valid"}
$prcs|?{-not $_.status -eq "valid"}
How would I go about making the last two lines smaller and/or more efficient? Complete newbie here!

If you're running V4, you can use the Split option of the Where() method:
$Valid,$NotValid = $prcs.where({$_.status -eq "valid"},'Split')

Related

Powershell script. More efficient way to perform nested foreach loops? [duplicate]

This question already has answers here:
In PowerShell, what's the best way to join two tables into one?
(5 answers)
Closed 5 months ago.
Good day.
I wrote a script that imports Excel files and then compares the rows. Each file contains about 13K rows. It is taking about 3 hours to process, which seems too long. This is happening because I am looping through every 13K rows from fileb for each row in filea.
Is there a more efficient way to do this?
Here is sample code:
#Import rows as customObject
rowsa = Import-Excel $filea
rowsb = Import-Excel $fileb
#Loop through each filea rows
foreach ($rowa in $rowsa)
{
#Loop through each fileb row. If the upc code matches rowa, check if other fields match
foreach ($rowb in $rowsb)
{
$rowb | Where-Object -Property "UPC Code" -Like $rowa.upc |
Foreach-Object {
if (( $rowa.uom2 -eq 'INP') -and ( $rowb.'Split Quantity' -ne $rowa.qty1in2 ))
{
#Do Something
}
}
}
Seems like you can leverage Group-Object -AsHashtable for this. See about Hash Tables for more info on why this should be faster.
$mapB = Import-Excel $fileb | Group-Object 'UPC Code' -AsHashTable -AsString
foreach($row in Import-Excel $filea) {
if($mapB.ContainsKey($row.upc)) {
$value = $mapB[$row.upc]
if($row.uom2 -eq 'INP' -and $row.qty1in2 -ne $value.'Split Quantity') {
$value # => has the row matching on UPC (FileA) / UPC Code (FileB)
$row # => current row in FileA
}
}
}
A few tricks:
The Object Pipeline may be easy, but it's not as fast as a statement
Try changing your code use to foreach statements, not Where-Object and Foreach-Object.
Use Hashtables to group.
While you can use Group-Object to do this, Group-Object suffers from the same performance problems as anything else in the pipeline.
Try to limit looping within looping.
As a general rule, looping within looping will be o(n^2). If you can avoid loops within loops, this is great. So switching the code around to loop thru A, then loop thru B, will be more efficient. So will exiting your loops as quickly as possible.
Consider using a benchmarking tool
There's a little module I make called Benchpress that can help you test multiple approaches to see which is faster. The Benchpress docs have a number of general PowerShell performance benchmarks to help you determine the fastest way to script a given thing.
Updated Script Below:
#Import rows as customObject
$rowsa = Import-Excel $filea
$rowsb = Import-Excel $fileb
$rowAByUPC = #{}
foreach ($rowA in $rowsa) {
# This assumes there will only be one row per UPC.
# If there is more than one, you may need to make a list here instead
$rowAByUPC[$rowA.UPC] = $rowA
}
foreach ($rowB in $rowsB) {
# Skip any rows in B that don't have a UPC code.
$rowBUPC = $rowB."UPC Code"
if (-not $rowBUPC) { continue }
$RowA = $rowAByUPC[$rowBUPC]
# It seems only rows that have 'INP' in uom2 are important
# so continue if missing
if ($rowA.uom2 -ne 'INP') { continue }
if ($rowA.qty1in2 -ne $rowB.'Split Quantity') {
# Do what you came here to do.
}
}
Please note that as you have not shared the code within the matching condition, you may need to take the advice contained in this answer and apply it to the inner code.

Powershell .Where() method with multiple properties

I have a GenericList of Hashtables, and I need to test for the existence of a record based on two properties. In my hash table, I have two records that share one property value, but are different on another property value.
Specifically, DisplayName of both is Autodesk Content for Revit 2023
But UninstallString for one is MsiExec.exe /X{GUID} while the other is C:\Program Files\Autodesk\AdODIS\V1\Installer.exe followed by a few hundred characters of other info
I want to select only the one with AdODIS in the UninstallString. And I would like to do it without a loop, and specifically using the .Where() method rather than the pipeline and Where-Object.
There are also MANY other records.
I CAN select just based on one property, like this...
$rawKeys.Where({$_.displayName -eq 'Autodesk Content for Revit 2023'})
And I get the appropriate two records returned. However, when I try expanding that to two properties with different criteria, like this...
$rawKeys.Where({($_.displayName -eq 'Autodesk Content for Revit 2023') -and ($_.uninstallString -like 'MsiExec.exe*')})
nothing is returned. I also tried chaining the .Where() calls, like this...
$rawKeys.Where({$_.displayName -eq 'Autodesk Content for Revit 2023'}).Where({$_.uninstallString -like 'MsiExec.exe*'})
and again, nothing returned.
just to be sure the second condition is working, I tried...
$rawKeys.Where({$_.uninstallString -like 'MsiExec.exe*'})
and got multiple records returned, as expected.
I found [this][1] that talk about doing it with Where-Object, and applying that approach to the method was my first attempt. But I have yet to see either an example of doing it with .Where() or something specifically saying .Where() is limited to one conditional.
So, am I just doing something wrong? Or is this actually not possible with .Where() and I have no choice but to use the pipeline? And there I would have thought based on that link that some variation on...
$rawKeys | Where-Object {(($_.displayName -eq 'Autodesk Content for Revit 2023') -and ($_.uninstallString -like 'MsiExec.exe*'))}
would work, but that's failing too.
I also tried...
$rawKeys.Where({$_.displayName -eq 'Autodesk Content for Revit 2023'}) -and $rawKeys.Where({$_.uninstallString -like 'MsiExec.exe*'})
And THAT returns true, which for my current need is enough, but one: I would like to know if it can be done in a single method call, and two: I can imagine I will eventually want to get the record(s) back, rather than just a bool. Which is only possible with the single method call.
EDIT: OK, this is weird. I tried doing a minimal example of actual data, like this...
$rawKeys = New-Object System.Collections.Generic.List[Hashtable]
$rawKeys.Add(#{
displayName = 'Autodesk Content for Revit 2023'
uninstallString = 'C:\Program Files\Autodesk\AdODIS\V1\Installer.exe whatever else is here'
guid = '{019AEF66-C054-39BB-88AD-B2D8EA9BE40A}'
})
$rawKeys.Add(#{
displayName = 'Autodesk Content for Revit 2023'
uninstallString = 'MsiExec.exe /X{205C6D76-2023-0057-B227-DC6376F702DC}'
guid = '{205C6D76-2023-0057-B227-DC6376F702DC}'
})
and that WORKS. So somewhere in my real code I am changing the data, and for the life of me I can't see where it's happening. But it's happening. The ACTUAL data comes from the registry, with this code...
$uninstallKeyPaths = #('SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall')
$rawKeys = New-Object System.Collections.Generic.List[Hashtable]
$localMachineHive = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, 0)
foreach ($uninstallKeyPath in $uninstallKeyPaths) {
foreach ($uninstallKeyName in $localMachineHive.OpenSubKey($uninstallKeyPath).GetSubKeyNames()) {
if ($uninstallKeyPath -like '*Wow6432Node*') {
$bitness = 'x32'
} else {
$bitness = 'x64'
}
$uninstallKey = $localMachineHive.OpenSubKey("$uninstallKeyPath\$uninstallKeyName")
if (($displayName = $uninstallKey.GetValue('DisplayName')) -and ($displayVersion = $uninstallKey.GetValue('DisplayVersion')) -and
(($installDate = $uninstallKey.GetValue('InstallDate')) -or ($uninstallString = $uninstallKey.GetValue('UninstallString')))) {
$keyName = [System.IO.Path]::GetFileName($uninstallKey.Name)
$keyData = #{
displayName = $displayName
displayVersion = $displayVersion
guid = "$(if ($keyName -match $pattern.guid) {$keyName})" #$Null
publisher = $uninstallKey.GetValue('Publisher')
uninstallString = $uninstallString
installDate = $installDate
properties = (#($uninstallKey.GetValueNames()) | Sort-Object) -join ', '
type = $bitness
}
[void]$rawKeys.Add($keyData)
}
}
}
So, meaningless unless you actually have Autodesk Revit 2023 installed on your machine, but maybe someone sees where I am changing the data.
[1]: Where-object $_ matches multiple criterias

PowerShell return multiple values from if condition

I have a Powershell script returning data from an API which works fine as long as I only attempt to return one $device.realm, but I need multiple realms. I'm a newb to PS.
Any help here is really appreciated
Here is my code
$Output = forEach ($device in $devices) {
if ($device.realmName -eq 'Archive') {
[PSCustomObject]#{
HostName = $device.name
IPAddress = $device.primaryInterfaceAddress
Realm = $device.realmName
SerialNumbers = (($device.dynamicFields | where { $_.name -EQ "serial number" } | Select-Object -ExpandProperty values) -join "," | out-string).TrimEnd()
}| Select-Object Hostname,IPAddress,Realm,SerialNumbers | Export-csv C:\temp\Archive.csv -notype -Append
}
I need to return multiple $device.realms as in
if ($device.realmName -eq 'Archive' -and 'Default' -and 'Farms')
Once I add the additional -and's every realm is returned instead of just the one's I need to return.
I believe the issue at hand here is that the statement within the If block that you're querying as ($device.realmName -eq 'Archive' -and 'Default' -and 'Farms')
is not, when evaluated logically "Evaluate true if the device realmname is Archive, Default, or Farms." It is evaluating whether device.realmname is archive, and then just interpreting the two -ands in your example as true, as they are not querying a comparison, but just the presence of a non-null string. Not sure what is leading it to return everything, I'd have to see some more direct examples to be sure, but in my experience that is most common when you include an -or in a comparison pointing to a nonnull string, which will make the entire statement true.
What I would suggest is as follows: Use the regex operators built in to powershell for cases like this. You could use
if($device.realmname -eq 'Archive' -or $Device.realmname -eq 'farm' -or $device.realmname -eq 'Default')
which would, I believe, return what you are looking for, but I find it a bit complex. More complicated queries on a single property, I find, are easiest to do via -match, through something invoking the -match operator, which allows you to build a regex query statement that can include Or's or And's with a bit simpler of a synatax, like so:
if($Device.realmName -match 'Archive|Farm|Default')

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

Issue trying filter within Powershell

I am finishing a last piece of this mini-app where I'm pulling names from a text file. When pulling the names from a ..txt I am filtering out certain prefixes like *eft, *nsm with code like below.
$lines = (Get-Content C:\temp\PROD\Repeat.txt -totalcount 200)
$flines = $lines|?{$_ -notlike "*eft", "nsm*", "*" , "*" .... }
$objOutputBox.Text = $flines
The problem I'm having is that it is only grabbing the "*eft" and not the rest of them. I thought I could filter an array of strings with this structure? What am I missing here if you do not mind?
Thanks
You cannot apply -notlike like this. You'll have to use the operator multiple times:
-notlike '*eft' -notlike 'nsm*' ...
But a better way would probably be a regular expression:
-notmatch 'eft$|^nsm|...'