Artykuł

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 
    }
}

ForEach vs ForEach-Object

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.

Tags: , , , ,

This is a unique website which will require a more modern browser to work! Please upgrade today!