PowerShell

PowerShell – Returning one object from a function as an Array

Few weeks had passed since I've initially written PowerShell – Few tricks about HashTables and Arrays I wish I knew when I started. I was happily using my tips and tricks myself till today when I noticed a strange problem. Do you know how I showed you that you could use a comma to return Array with just one member (that otherwise would be unwrapped and end up a string)?

function Show-ThirdExample {
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    , $Output
}

Write-Color 'Example 3' -Color Cyan
$Value1 = Show-ThirdExample -Test 'one', 'two'
$Value1 -is [array]
$Value2 = Show-ThirdExample -Test 'one'
$Value2 -is [array]

Works great right? So I thought till today.

The problem

Today I wanted to use one of my functions (Get-Events) when working with ForEach-Object. You see I usually don't use ForEach-Object and I mostly do foreach, but today I wanted to show someone trick that I've shared on PowerShell – Everything you wanted to know about Event Logs and then some but instead of using Get-WinEvent I tried to use Get-Events.

$FilterHashTable = @{
    LogName   = 'Application'
    ID        = 1534
    StartTime = (Get-Date).AddHours(-1)
    EndTime   = Get-Date
}


$Events = Get-WinEvent -FilterHashtable $FilterHashTable | ForEach-Object {
    $Values = $_.Properties | ForEach-Object { $_.Value }
    
    # return a new object with the required information
    [PSCustomObject]@{
        Time      = $_.TimeCreated
        # index 0 contains the name of the update
        Event     = $Values[0]
        Component = $Values[1]
        Error     = $Values[2]
        User      = $_.UserId.Value
    }
}

$Events | Format-Table -AutoSize

The trick is related to how you can read hidden properties from Event, but for this post, we're carrying only about the use of ForEach-Object and how it's used in this case. It basically uses Get-WinEvent to get events from event log and as they show up it passes it thru pipeline creating PSCustomObject, finally saving it in $Events variable. Seems easy enough. But when I've run this on my own functions I was getting strange results.

$FilterHashTable = @{
    LogName = 'Security'
    ID      = 4625
}
Write-Color 'Get-WinEvent', ' - ', ' ForEach' -Color Yellow, White, Green
$WinEvent = Get-WinEvent -FilterHashtable $FilterHashTable -MaxEvents 3 -ComputerName 'AD1.AD.EVOTEC.XYZ'
foreach ($Event in $WinEvent) {
    $Event.TimeCreated.Count
    $Event.ProviderName.Count
}
Write-Color 'Get-WinEvent', ' - ', ' ForEach-Object' -Color Yellow, White, Green
Get-WinEvent -FilterHashtable $FilterHashTable -MaxEvents 3 -ComputerName 'AD1.AD.EVOTEC.XYZ' | ForEach-Object {
    $_.TimeCreated.Count
    $_.ProviderName.Count
}

The code above is a simplified version for testing behavior of ForEach-Object and ForEach. I asked for three events from Event Log and I'm simply checking Count for two variables.

Results aren't really surprising, and that's what I expected. The same test for Get-Events (PSEventViewer PowerShell Module) went slightly surprising.

$MyData = @{
    LogName   = 'Security'
    ID        = 4625
    Machine   = 'AD1.ad.evotec.xyz'
    MaxEvents = 3
}

Write-Color 'Get-Events', ' - ', ' ForEach' -Color Yellow, White, Green
$Events = Get-Events @MyData
foreach ($Event in $Events) {
    $Event.TimeCreated.Count
    $Event.ProviderName.Count
}
Write-Color 'Get-Events', ' - ', ' ForEach-Object' -Color Yellow, White, Green
Get-Events @MyData | ForEach-Object {
    $_.TimeCreated.Count
    $_.ProviderName.Count
}

For a moment I thought that maybe my PowerShell foo is failing me and I just don't know how to use ForEach properly but it seems it's related to comma operator that I was so happy to brag about earlier on. To confirm my suspicion about comma I've decided to use my old example.

function Show-ThirdExample {
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    , $Output
}

function Show-ThirdExample {
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    , $Output
}


Write-Color 'Example - ForEach (two elements)' -Color Cyan
$Value1 = Show-ThirdExample -Test 'one', 'two'
foreach ($Value in $Value1) {
    $Value.Count
}
Write-Color 'Example - Foreach (one element)' -Color Cyan
$Value2 = Show-ThirdExample -Test 'one'
foreach ($Value in $Value2) {
    $Value.Count
}
Write-Color 'Example - ForEach Object' -Color Cyan
Show-ThirdExample -Test 'one', 'two' | ForEach-Object {
    $_.Count
}

Testing my idea proves that for some reason (unknown to me) using a comma to preserve Array is making ForEach-Object unusable. ForEach works just fine. While I can continue using it that way, most likely someone will use ForEach-Object and will get wrong results. So how do I fix it?

function Show-ThirdExample {
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    @($Output)
}


Write-Color 'Example - ForEach (two elements)' -Color Cyan
$Value1 = Show-ThirdExample -Test 'one', 'two'
foreach ($Value in $Value1) {
    $Value.Count
}
Write-Color 'Example - Foreach (one element)' -Color Cyan
$Value2 = Show-ThirdExample -Test 'one'
foreach ($Value in $Value2) {
    $Value.Count
}
Write-Color 'Example - ForEach Object' -Color Cyan
Show-ThirdExample -Test 'one', 'two' | ForEach-Object {
    $_.Count
}

As you can see above, this gives proper result when it comes to ForEach-Object vs. ForEach. But if we will check what is returned it isn't Array for one element anymore.

Getting an Array for one element and at the same time preserving ForEach-Object/ForEach consistency

So how do I get what I need? An Array for one or multiple elements and at the same time ForEach-Object behaving the same way as ForEach?

function Show-ThirdExample {
    [OutputType([Array])]
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    $Output
}

Clear-Host
Write-Color 'Example - ForEach (two elements)' -Color Cyan
$Value1 = Show-ThirdExample -Test 'one', 'two'
foreach ($Value in $Value1) {
    $Value.Count
}
Write-Color 'Example - Foreach (one element)' -Color Cyan
[Array] $Value2 = Show-ThirdExample -Test 'one'
foreach ($Value in $Value2) {
    $Value.Count
}
Write-Color 'Example - ForEach Object' -Color Cyan
Show-ThirdExample -Test 'one', 'two' | ForEach-Object {
    $_.Count
}

Write-Color 'Example - Verify if still array' -Color  Yellow
$Value1 -is [Array]
$Value2 -is [Array]
Write-Color 'Example - Verify if string (one element)' -Color Yellow
$Value2 -is [string]

We tell it that the value returned from a function is of type Array. As you can notice above I've removed any comma or wrapping into Array in function in favor of defining type later on and it seems to work just the way  I want it to. Alternatively, you could always go for returning an object with a comma only if it's a single element. This way you get both of worlds.

function Show-ThirdExample {
    [OutputType([Array])]
    param(
        [string[]] $Test
    )  
    [Array] $Output = foreach ($my in $Test) {
        $my
    }
    # I want to do something with value before returning
    if ($Output -is [array]) {
        Write-Color 'Array' -Color Green
    }
    # Actually returning
    if ($Output.Count -eq 1) {
        , $Output
    } else { 
        $Output 
    }
}

I still have no explanation why comma operator does what it does when it comes to ForEach-Object so if you ever read this and have an answer feel free to contact me and I'll be more than happy to update this article.

This post was last modified on 8 marca, 2019 11:37

Przemyslaw Klys

System Architect with over 14 years of experience in the IT field. Skilled, among others, in Active Directory, Microsoft Exchange and Office 365. Profoundly interested in PowerShell. Software geek.

Share
Published by
Przemyslaw Klys

Recent Posts

Upgrade Azure Active Directory Connect fails with unexpected error

Today, I made the decision to upgrade my test environment and update the version of…

2 miesiące ago

Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster

Have you ever looked at your Active Directory and wondered, "Why do I still have…

3 miesiące ago

Active Directory Replication Summary to your Email or Microsoft Teams

Active Directory replication is a critical process that ensures the consistent and up-to-date state of…

7 miesięcy ago

Syncing Global Address List (GAL) to personal contacts and between Office 365 tenants with PowerShell

Hey there! Today, I wanted to introduce you to one of the small but excellent…

12 miesięcy ago

Active Directory Health Check using Microsoft Entra Connect Health Service

Active Directory (AD) is crucial in managing identities and resources within an organization. Ensuring its…

1 rok ago

Seamless HTML Report Creation: Harness the Power of Markdown with PSWriteHTML PowerShell Module

In today's digital age, the ability to create compelling and informative HTML reports and documents…

1 rok ago