rstolpe
rstolpe

Reputation: 1

Foreach loop displays everything in same PSCustomObject

I have a foreach loop that should collect all of the user profiles in one PSCustomObject for each computer and display it. But that's not what happens it loops but all of the profiles are displayed in one and the same PSCustomObject and all show in the last output.

Example; If I want to display all the user profiles for Computer1 and Computer2 it displays all of the user profiles in the Computer2 row and nothing on the Computer1 row. It merge all information into the last display. See picture for more clarity.

Function Get-UserProfiles {
    [CmdletBinding()]
    Param(
        [string]$ComputerName = "localhost",
        [array]$ExcludedProfiles
    )
    foreach ($Computer in $ComputerName.Split(",").Trim()) {
        Write-Host "`n== All profiles on $($Computer) ==`n"
        try {
            Get-CimInstance -ComputerName $Computer -className Win32_UserProfile | Where-Object { (-Not ($_.Special)) } | Foreach-Object {
                if (-Not ($_.LocalPath.split('\')[-1] -in $ExcludedProfiles)) {
                    [PSCustomObject]@{
                        'UserName'               = $_.LocalPath.split('\')[-1]
                        'Profile path'           = $_.LocalPath
                        'Last used'              = ($_.LastUseTime -as [DateTime]).ToString("yyyy-MM-dd HH:mm")
                        'Is the profile active?' = $_.Loaded
                    }
                }
            }
        }
        catch {
            Write-Host "$($PSItem.Exception)"
            break
        }
    }
}

Picture that shows the output and what I mean

enter image description here

Upvotes: 0

Views: 557

Answers (1)

zett42
zett42

Reputation: 27806

The observed behavior is due to delayed output behaviour of Format-Table, which is implicitly used by your code. See this answer for an in-depth explanation.

Here is a simplified example, that when pasted into the console, exhibits the same behavior as your code:

Write-Host "== first =="; [pscustomobject] @{ one = '1a'; two = '2a'; three = '3a' }; Write-Host "== second =="; [pscustomobject] @{ one = '1b'; two = '2b'; three = '3b' }

Output:

== first ==

== second ==
one two three
--- --- -----
1a  2a  3a
1b  2b  3b

To fix the code at hand, simply use Format-Table explicitly:

Function Get-UserProfiles {
[CmdletBinding()]
Param(
    [string]$ComputerName = "localhost",
    [array]$ExcludedProfiles
)
foreach ($Computer in $ComputerName.Split(",").Trim()) {
    Write-Host "`n== All profiles on $($Computer) ==`n"
    try {
        Get-CimInstance -ComputerName $Computer -className Win32_UserProfile | Where-Object { (-Not ($_.Special)) } | Foreach-Object {
            if (-Not ($_.LocalPath.split('\')[-1] -in $ExcludedProfiles)) {
                [PSCustomObject]@{
                    'UserName'               = $_.LocalPath.split('\')[-1]
                    'Profile path'           = $_.LocalPath
                    'Last used'              = ($_.LastUseTime -as [DateTime]).ToString("yyyy-MM-dd HH:mm")
                    'Is the profile active?' = $_.Loaded
                }
            }
        } | Format-Table   # <<<<<<< ADDED <<<<<<<
    }
    catch {
        Write-Host "$($PSItem.Exception)"
        break
    }
}

While this should succeed in producing the desired output, it is not a good practice to intermingle code that creates objects with code that formats objects for display purposes only, such as Format-Table and Write-Host. You end up with an inflexible function that is "closed", as it cannot be used for further processing of the data (well, not impossible, but you'd have to parse the data in its displayed form, which is cumbersome and error-prone).

Instead, focus on the data and let the function output only objects. Think about how to display these objects later. The formatting could be done by another function, piped to the function that produces the objects.

Simplified example (NOT recommended):

Function Get-MyObjects {
    Write-Host "== first ==" 
    [pscustomobject] @{ one = '1a'; two = '2a'; three = '3a' } | Format-Table
    Write-Host "== second =="
    [pscustomobject] @{ one = '1b'; two = '2b'; three = '3b' } | Format-Table
}

# Create and display the objects, no further processing possible
Get-MyObjects  

Simplified example (recommended):

Function Get-MyObjects {
    # This outputs an array of two objects.
    # Note how the headers 'FIRST' and 'SECOND' have become data as well!
    [pscustomobject] @{ group = 'FIRST'; one = '1a'; two = '2a'; three = '3a' }
    [pscustomobject] @{ group = 'SECOND'; one = '1b'; two = '2b'; three = '3b' }
}

# Create objects, then pipe to `Format-Table` for display.
Get-MyObjects | Format-Table -GroupBy group -Property one, two, three

# We can still process the objects further:
$objects = Get-MyObjects
$objects | Where-Object group -eq FIRST | ForEach-Object { $_.one = 'foo' }
$objects | Format-Table -GroupBy group -Property one, two, three

Upvotes: 2

Related Questions