Can you have multiple IN conditions in a PowerShell ForEach loop? - powershell

I have the following sample code in a function:
[array]$ARR = $null
foreach ($file in $fileTable.identical)
{
[hashtable]$HT=#{
'FileName' = $file.Name
'AppName' = $file.App
'GroupName' = $file.Group
'Valid' = $true
}
$ARR += $HT
}
foreach ($file in $fileTable.removed)
{
[hashtable]$HT=#{
'FileName' = $file.Name
'AppName' = $file.App
'GroupName' = $file.Group
'Valid' = $false
}
$ARR += $HT
}
foreach ($file in $fileTable.modified)
{
[hashtable]$HT=#{
'FileName' = $file.Name
'AppName' = $file.App
'GroupName' = $file.Group
'Valid' = $false
}
$ARR += $HT
}
return $ARR
+3 more foreach loops for other $fileTable.[properties] where 'Valid' = $false as well.
Instead of having to repeat the block of code multiple times, I want to do something like:
foreach (($file in $fileTable.removed) -and ($file in $fileTable.modified))
{
[hashtable]$HT=#{
'FileName' = $file.Name
'AppName' = $file.App
'GroupName' = $file.Group
'Valid' = $false
}
}
So only variable different in the hashtable will be $value.
$fileTable is a pscustomobject with a few custom properties like identical, modified, added, removed.
I know what I want is not possible in foreach loops but I'm looking for a similar solution to reduce the number of lines of code. Any help would be appreciated :)
Thanks!

Combining your and PetSerAls approaches.
Edit: incorporated #mklement0s hint
$ARR = foreach($Variant in 'identical', 'removed', 'modified'){
$fileTable.$Variant | ForEach-Object{
[PSCustomObject]#{
'FileName' = $_.Name
'AppName' = $_.App
'GroupName' = $_.Group
# 'Valid' = if($Variant -eq 'identical'){$True} else {$False}
'Valid' = $Variant -eq 'identical'
}
}
}

Related

Is there a way to speed up dynamic member look-up in ps-objects

In the following code, most time is spent in $v = $proc.$columnName and I am wondering if there
is a way to speed up looking up the objects's members's values.
In the code below, I have chosen $objs to be the result of get-process but in my case, $objs could be an array of any type of objects, thus the need to look up the objects's members dynamically.
$objs= get-process
$columnNames = #()
foreach ($member in ($objs | get-member -memberType property, noteproperty)) {
[string]$name = $member.name
$columnNames += $name
}
[Int64 ] $sum = 0
[string] $columnName = ''
foreach ($obj in $objs) {
foreach ($columnName in $columnNames) {
$v = $obj.$columnName
# $v = $obj.psObject.members.Item($columnName).value
if ($v -eq $null) {
}
elseif ($v -is [System.IntPtr]) {
$sum = $sum + ($v -as [int64] )
}
elseif ($v -is [System.Int64] -or $v -is [System.Int32]) {
$sum = $sum + $v
}
}
}
"sum = $sum"
Perhaps there are more ways to speed this up, but below I have taken out the unnecessary bits:
$objs= Get-Process
$columnNames = ($objs | Get-Member -MemberType property, noteproperty).Name
[Int64 ] $sum = 0
foreach ($obj in $objs) {
foreach ($v in $columnNames) {
if ($obj.$v -as [int64]) { $sum += [int64]$obj.$v }
}
}
"sum = $sum"
This is a hidden property on PSObjects called .PSObject that way too many people don't know about, including myself until a few weeks ago despite spending thousands of hours working with PowerShell. From there you can use .where to SIGNIFICANTLY increase filtering performance.
New Code Block
New-Variable -Force -Name:'Processes' -value:(Get-Process)
New-Variable -Force -Name:'Sum' -Value:([Int64]$Null)
ForEach ($Process in $Processes) {
$Process.PSObject.Properties.Where({
($_.MemberType -in #('Property','NoteProperty')) -and
($_.TypeNameOfValue -in #('System.IntPtr','System.Int64','System.Int32')) -and
(-Not [String]::IsNullOrWhiteSpace($_.value))
}) |ForEach-Object {
$Sum = $Sum + ($_.value -as [Int64])
}
}
"Sum = $Sum"
Comparison Result
Name Value
---- -----
BlockBTime 9.18
SameResult True
BlockATime 1.52
BlockASum 1037197387512388
Difference Block A (New Code) is ~7.66s faster than Block B (Old Code)
BlockBSum 1037197387512388
Validation & Comparison
#Stopwatch
New-Variable -Force -Name:'StopWatch' -Value:(New-Object -TypeName 'System.Diagnostics.Stopwatch')
New-Variable -Force -Name:'Result' -Value:#{
BlockATime = [Int]0
BlockASum = [Int64]0
BlockBTime = [Int]0
BlockBSum = [Int64]0
Difference = $Null
SameResult = $Null
}
New-Variable -Force -Name:'Processes' -value:(Get-Process)
$StopWatch.Restart()
New-Variable -Force -Name:'Sum' -Value:([Int64]$Null)
ForEach ($Process in $Processes) {
$Process.PSObject.Properties.Where({
($_.MemberType -in #('Property','NoteProperty')) -and
($_.TypeNameOfValue -in #('System.IntPtr','System.Int64','System.Int32')) -and
(-Not [String]::IsNullOrWhiteSpace($_.value))
}) |ForEach-Object {
$Sum = $Sum + ($_.value -as [Int64])
}
}
$Result.BlockATime = [math]::Round($StopWatch.Elapsed.TotalSeconds,2)
$Result.BlockASum = $Sum
$objs= $Processes
$StopWatch.Restart()
$columnNames = #()
foreach ($member in ($objs | get-member -memberType property, noteproperty)) {
[string]$name = $member.name
$columnNames += $name
}
[Int64 ] $sum = 0
[string] $columnName = ''
foreach ($obj in $objs) {
foreach ($columnName in $columnNames) {
$v = $obj.$columnName
# $v = $obj.psObject.members.Item($columnName).value
if ($v -eq $null) {
}
elseif ($v -is [System.IntPtr]) {
$sum = $sum + ($v -as [int64] )
}
elseif ($v -is [System.Int64] -or $v -is [System.Int32]) {
$sum = $sum + $v
}
}
}
$Result.BlockbTime = [math]::Round($StopWatch.Elapsed.TotalSeconds,2)
$Result.BlockbSum = $Sum
#Which block is faster?
If ($Result.BlockATime -lt $Result.BlockBTime) {
$Result.Difference = "Block A (New Code) is ~$(($Result.BlockBTime - $Result.BlockATime))s faster than Block B (Old Code)"
} Else {
$Result.Difference = "Block A (New Code) is ~$(($Result.BlockBTime - $Result.BlockATime))s Slower than Block B (Old Code)"
}
#Are the results the same?
If ($Result.BlockASum -eq $Result.BlockBSum) {
$Result.SameResult = $True
}

Loop Confusion Comparing Objects

I am trying to create my own service comparison script. I see some online but want to do this myself. I've only gotten so far. I keep getting confused.
The desired output is with the following format. It doesn't even have to show what's different. I just want to see what the previous state was compared to the current state. I did a compare-object and it didn't give me the format I desired. I then thought maybe I should just do two nested loops and create a new object with the states I want in it. It didn't work out correctly, it returns an array. So then I thought, maybe a for loop in the foreach loop... I keep confusing myself and it's so close.
You have to provide a csv with some services to compare to to make this work as part of it's paramaters.
Usage
Inspect-ServiceSnapshot -SnapshotPath "C:\YourPath"
Desired output
Name CurrentState PreviousState
app1 Running Stopped
Code So Far
function Inspect-ServiceSnapshot {
[CmdletBinding()]
param (
#Snapshot
[Parameter(Mandatory=$true)]
[ValidatePattern("C:")]
[string]
$SnapshotPath,
# timer
[Parameter(Mandatory=$false)]
[int]
$TimeToWait
)
if($TimeToWait -ne $null) {
Start-Sleep -Seconds $TimeToWait
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (get-service)) {
foreach($oldItem in $old) {
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = $oldItem.status
}
$list += $object
}
}
$list
} else {
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (get-service)) {
foreach($oldItem in $old) {
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = $oldItem.status
}
$list += $object
}
}
$list
}
}
This should do it because it is actually checking the value for the old service to be the same as the one Get-Service provides at a certain time.
function Inspect-ServiceSnapshot {
[CmdletBinding()]
param (
#Snapshot
[Parameter(Mandatory=$true)]
[ValidateScript({Test-Path $_ -PathType Leaf})]
[string]$SnapshotPath,
# timer
[Parameter(Mandatory=$false)]
[int]$TimeToWait = 0
)
if($TimeToWait) { Start-Sleep -Seconds $TimeToWait }
$list = #()
$old = Import-Csv -Path $SnapshotPath
foreach($entry in (Get-Service)) {
# make sure we are dealing with the SAME service
$oldItem = $old | Where-Object { $_.Name -eq $entry.Name }
$object = New-Object -TypeName psobject -Property #{
Name = $entry.Name
CurrentStatus = $entry.status
DisplayName = $entry.displayname
PreviousStatus = if ($oldItem) { $oldItem.status } else { 'Unknown' }
}
$list += $object
}
$list
}
This answer is assuming there are no new services added on a regular basis.
You almost got it! You can forgo the nested loops, get rid of the else block (it is redundant), and use an index loop. When you're using nested loops like that, you are iterating through all of the array elements in the $old array every time you iterate through one of the (get-service) objects. This can cause issues when your arrays include thousands of objects.
You can easily get to what you want by using a for loop.
e.g.
if($TimeToWait -ne $null) {
Start-Sleep -Seconds $TimeToWait
}
$list = #();
$old = Import-Csv -Path $SnapshotPath | Sort-Object Name;
$new = Get-Service | Sort-Object Name;
for ($i -eq 0; $i -lt $old.length -or $i -lt $new.length; $i++) {
$object = New-Object -TypeName psobject -Property #{
Name = $new[$i].Name
CurrentStatus = $new[$i].status
DisplayName = $new[$i].displayname
PreviousStatus = $old[$i].status
}
$list += $object;
}
$list;
A lot of your code is redundant and you can just do this all in one go. Since you're not technically comparing any objects, you can just populate the fields as they go.

Improving the speed of Get-FileMetaData

I'm currently using the below script taken from scriptingguys.com (all credit to them, I just added the bottom 2 lines.) That takes a directory and pulls the file path and comments field from the meta data of the files. Currently the script take's a little over 1.5 minutes to fully run. Is there anyway to speed this up or use a different method to get this data?
I am using this script at the start of some software I have written and 1.5+ minutes is too long for the script to complete. Any thoughts/comments?
Function Get-FileMetaData
{
Param([string[]]$folder)
foreach($sFolder in $folder)
{
$a = 0
$objShell = New-Object -ComObject Shell.Application
$objFolder = $objShell.namespace($sFolder)
foreach ($File in $objFolder.items())
{
$FileMetaData = New-Object PSOBJECT
for ($a ; $a -le 266; $a++)
{
if($objFolder.getDetailsOf($File, $a))
{
$hash += #{$($objFolder.getDetailsOf($objFolder.items, $a)) =
$($objFolder.getDetailsOf($File, $a)) }
$FileMetaData | Add-Member $hash
$hash.clear()
} #end if
} #end for
$a=0
$FileMetaData
} #end foreach $file
} #end foreach $sfolder
} #end Get-FileMetaData
$fileMetaData = Get-FileMetaData -folder "C:\Pics" | select 'Name', 'Path', 'Comments' | Sort-Object 'Name'
$fileMetaData | select 'Name', 'Path', 'Comments' | Export-CSV "C:\SCRIPTS\TestDirectory.txt" -encoding Utf8 -NoTypeInformation
Solved by wOxxOm, thanks for your help! Running the below and now working.
Function Get-FileMetaData(
[string[]]$folders,
[string[]]$properties
) {
$shellApp = New-Object -ComObject Shell.Application
$supportsOrdered = $PSVersionTable.PSVersion.Major -ge 3
$hash = if ($supportsOrdered) { [ordered]#{} } else { #{} }
# walk the folders and get the properties by index found above
$folders | ForEach {
$shellFolder = $shellApp.namespace($_)
# get all headers and find their indexes
$allProps = #{}
foreach ($index in 0..266) {
$allProps[$shellFolder.getDetailsOf($shellFolder.items, $index)] = $index
}
$shellFolder.items() | ForEach {
$file = $_
$hash.Clear()
foreach ($prop in $properties) {
if (($index = $allProps[$prop]) -ne $null) {
if ($value = $shellFolder.getDetailsOf($file, $index)) {
$hash[$prop] = $value
}
}
}
if ($supportsOrdered) {
[PSCustomObject]$hash
} else {
Select $properties -inputObject (
New-Object PSObject -Property $hash
)
}
}
}
}
Get-FileMetaData -folders 'C:\PICS' -properties Name, Path, Comments | Sort-Object Name |
select Name, Path, Comments | Export-Csv 'C:\Scripts\test.txt' -encoding UTF8 -NoTypeInformation
getDetailsOf is slow, and your code needlessly invokes it 267 times for each file when you only need it for 3 properties.
Collect the property names just once at the start of the function, don't do it on every file
Add-Member is slow. Don't invoke it on every property. Collect all found properties in a hashtable and pass it once to Add-Member or, since you create an empty object, directly to New-Object. To enforce the order of properties use Select-Object in PowerShell 2. Note, PowerShell 3.0 and newer support [ordered] and [PSCustomObject] typecast (see the code below).
Use pipelining instead of foreach statements so that the results appear immediately
Files are already sorted by name, at least on NTFS file system in Windows, so no need to sort.
Function Get-FileMetaData(
[string[]]$folders,
[string[]]$properties
) {
$shellApp = New-Object -ComObject Shell.Application
# get all headers and find their indexes
$shellFolder = $shellApp.namespace($folders[0])
$allProps = #{}
foreach ($index in 0..266) {
$allProps[$shellFolder.getDetailsOf($shellFolder.items, $index)] = $index
}
$supportsOrdered = $PSVersionTable.PSVersion.Major -ge 3
$hash = if ($supportsOrdered) { [ordered]#{} } else { #{} }
# walk the folders and get the properties by index found above
$folders | ForEach {
$shellFolder = $shellApp.namespace($_)
$shellFolder.items() | ForEach {
$file = $_
$hash.Clear()
foreach ($prop in $properties) {
if (($index = $allProps[$prop]) -ne $null) {
$hash[$prop] = $shellFolder.getDetailsOf($file, $index)
}
}
if ($supportsOrdered) {
[PSCustomObject]$hash
} else {
Select $properties -inputObject (
New-Object PSObject -Property $hash
)
}
}
}
}
Usage example 1:
Get-FileMetaData -folders 'r:\folder1', 'r:\folder2' -properties Name, Path, Comments
Usage example 2:
Get-FileMetaData -folders 'r:\folder1', 'r:\folder2' -properties Name, Path, Comments |
Export-Csv r:\results.csv -encoding UTF8 -NoTypeInformation
Usage example 3 gets all properties, which is slow:
Get-FileMetaData -folders 'r:\folder1', 'r:\folder2'

PSCustomObject to Hashtable

What is the easiest way to convert a PSCustomObject to a Hashtable? It displays just like one with the splat operator, curly braces and what appear to be key value pairs. When I try to cast it to [Hashtable] it doesn't work. I also tried .toString() and the assigned variable says its a string but displays nothing - any ideas?
Shouldn't be too hard. Something like this should do the trick:
# Create a PSCustomObject (ironically using a hashtable)
$ht1 = #{ A = 'a'; B = 'b'; DateTime = Get-Date }
$theObject = new-object psobject -Property $ht1
# Convert the PSCustomObject back to a hashtable
$ht2 = #{}
$theObject.psobject.properties | Foreach { $ht2[$_.Name] = $_.Value }
Keith already gave you the answer, this is just another way of doing the same with a one-liner:
$psobject.psobject.properties | foreach -begin {$h=#{}} -process {$h."$($_.Name)" = $_.Value} -end {$h}
Here's a version that works with nested hashtables / arrays as well (which is useful if you're trying to do this with DSC ConfigurationData):
function ConvertPSObjectToHashtable
{
param (
[Parameter(ValueFromPipeline)]
$InputObject
)
process
{
if ($null -eq $InputObject) { return $null }
if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string])
{
$collection = #(
foreach ($object in $InputObject) { ConvertPSObjectToHashtable $object }
)
Write-Output -NoEnumerate $collection
}
elseif ($InputObject -is [psobject])
{
$hash = #{}
foreach ($property in $InputObject.PSObject.Properties)
{
$hash[$property.Name] = ConvertPSObjectToHashtable $property.Value
}
$hash
}
else
{
$InputObject
}
}
}
My extremely lazy approach, enabled by a new feature in PowerShell 6:
$myhashtable = $mypscustomobject | ConvertTo-Json | ConvertFrom-Json -AsHashTable
This works for PSCustomObjects created by ConvertFrom_Json.
Function ConvertConvertFrom-JsonPSCustomObjectToHash($obj)
{
$hash = #{}
$obj | Get-Member -MemberType Properties | SELECT -exp "Name" | % {
$hash[$_] = ($obj | SELECT -exp $_)
}
$hash
}
Disclaimer: I barely understand PowerShell so this is probably not as clean as it could be. But it works (for one level only).
My code:
function PSCustomObjectConvertToHashtable() {
param(
[Parameter(ValueFromPipeline)]
$object
)
if ( $object -eq $null ) { return $null }
if ( $object -is [psobject] ) {
$result = #{}
$items = $object | Get-Member -MemberType NoteProperty
foreach( $item in $items ) {
$key = $item.Name
$value = PSCustomObjectConvertToHashtable -object $object.$key
$result.Add($key, $value)
}
return $result
} elseif ($object -is [array]) {
$result = [object[]]::new($object.Count)
for ($i = 0; $i -lt $object.Count; $i++) {
$result[$i] = (PSCustomObjectConvertToHashtable -object $object[$i])
}
return ,$result
} else {
return $object
}
}
For simple [PSCustomObject] to [Hashtable] conversion Keith's Answer works best.
However if you need more options you can use
function ConvertTo-Hashtable {
<#
.Synopsis
Converts an object to a hashtable
.DESCRIPTION
PowerShell v4 seems to have trouble casting some objects to Hashtable.
This function is a workaround to convert PS Objects to [Hashtable]
.LINK
https://github.com/alainQtec/.files/blob/main/src/scripts/Converters/ConvertTo-Hashtable.ps1
.NOTES
Base ref: https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/turning-objects-into-hash-tables-2
#>
PARAM(
# The object to convert to a hashtable
[Parameter(ValueFromPipeline = $true, Mandatory = $true)]
$InputObject,
# Forces the values to be strings and converts them by running them through Out-String
[switch]$AsString,
# If set, empty properties are Included
[switch]$AllowNulls,
# Make each hashtable to have it's own set of properties, otherwise,
# (default) each InputObject is normalized to the properties on the first object in the pipeline
[switch]$DontNormalize
)
BEGIN {
$headers = #()
}
PROCESS {
if (!$headers -or $DontNormalize) {
$headers = $InputObject | Get-Member -type Properties | Select-Object -expand name
}
$OutputHash = #{}
if ($AsString) {
foreach ($col in $headers) {
if ($AllowNulls -or ($InputObject.$col -is [bool] -or ($InputObject.$col))) {
$OutputHash.$col = $InputObject.$col | Out-String -Width 9999 | ForEach-Object { $_.Trim() }
}
}
} else {
foreach ($col in $headers) {
if ($AllowNulls -or ($InputObject.$col -is [bool] -or ($InputObject.$col))) {
$OutputHash.$col = $InputObject.$col
}
}
}
}
END {
return $OutputHash
}
}
Maybe this is overkill but I hope it Helps
Today, the "easiest way" to convert PSCustomObject to Hashtable would be so:
$custom_obj | ConvertTo-HashtableFromPsCustomObject
OR
[hashtable]$custom_obj
Conversely, you can convert a Hashtable to PSCustomObject using:
[PSCustomObject]$hash_table
Only snag is, these nifty options may not be available in older versions of PS

Convert a list of key value pairs to a hashtable

What is the best way to convert a List to a Hashtable?
Say I have a list like ("Key",$value,"Key2",$value2)
What is the shortest syntax to convert it into a Hashtable?
Try the following
$table = new-object System.Collections.Hashtable
for ( $i = 0; $i -lt $list.Length; $i += 2 ) {
$table.Add($list[$i],$list[$i+1]);
}
Function ConvertTo-Hashtable($list) {
$h = #{}
while($list) {
$head, $next, $list = $list
$h.$head = $next
}
$h
}
ConvertTo-Hashtable ("Key",1,"Key2",2)
If your list is composed of NameValuePair objects, then you can use:
Function ConvertListOfNVPTo-Hashtable($list) {
$h = #{}
$list | ForEach-Object {
$h[$_.Name] = $_.Value
}
return $h
}
$h = #{}
0..($l.count - 1) | ? {$_ -band 1} | % {$h.Add($l[$_-1],$l[$_])}
$h = #{}
0..($l.count - 1) | ? {$_ -band 1} | % {$h.($l[$_-1]) = $l[$_]}
$h = #{}
$i = 0
while ($i -lt $l.count) {$h.Add($l[$i++],$l[$i++])}
How about:
$ht = #{}
$key = "";
("Key",5,"Key2",6) | foreach `
{
if($key)
{
$ht.$key = $_;
$key="";
} else
{$key=$_}
}
function Select-Hashtable {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
[PSObject[]]$InputObject,
[Parameter(Mandatory)]
[string]$Property
)
begin { $hashtable = [Hashtable]::new() }
process { $InputObject | %{ $hashtable[$_."$Property"] = $_} }
end { $hashtable }
}
then (a pointless example):
$items = Get-ChildItem -Path $path | Select-Hashtable -Property Name
$documents = $items['Documents']