Ryan Taylor
Ryan Taylor

Reputation: 8892

Different behavior when writing PowerShell objects to the console

I have a function Get-Projects that returns an array of objects. I want to print these to the console so the user can select which project they are interested in. However, I have four scenarios only two of which print the desired/expected result.

Scenario 1 - Tabular Output, No Function

When I simply "return" the projects like so they are printed out as expected in tabular fashion. This is the desired format.

$projects = Get-Projects
$projects

# Console Output
id      name         children                                                      
--      ----         --------                                                      
1       Project 1    1 {@id=2; name=Project 2}
3       Project 3    3 {@id=4; name=Project 4}

Scenario Two - No Output w/ Write-Projects Function

I created a function named Write-Projects to encapsulate the formatting behavior in case I decide to change how the formatting down the road. Yet, when I do this nothing is printed to the console.

Function Write-Projects
{
    Param([Object[]] $projects)

    $projects
}

$projects = Get-Projects
Write-Projects $projects

# No Console Output

Scenario 3 - String Output w/ Write-Projects Function

If I modify Write-Projects to use Write-Host $projects I do get console output but not what I expected. It's appears to be the string representation of my Object array.

Function Write-Projects
{
    Param([Object[]] $projects)

    Write-Host $projects
}

$projects = Get-Projects
Write-Projects $projects

# Console Output
@{id=1; name=Project 1; children=System.Object[]} @{id=2; name=Project 2; children=System.Object[]}

Scenario 4 - Tabular Output w/ Write-Projects Function

I discovered this question which solves the problem but I am uncertain why. Essentially my Write-Projects method now looks like this.

Function Write-Projects
{
    Param([Object[]] $projects)

    Write-Host ($projects | Format-Table | Out-String)
}

$projects = Get-Projects
Write-Projects $projects

# Console Output
id      name         children                                                      
--      ----         --------                                                      
1       Project 1    1 {@id=2; name=Project 2}
3       Project 3    3 {@id=4; name=Project 4}

What is happening in each of these scenarios and why I am getting the output as described?

Upvotes: 1

Views: 818

Answers (2)

Rakhesh Sasidharan
Rakhesh Sasidharan

Reputation: 1063

I am not sure why scenario 2 doesn't work. Just to test I made some custom objects and tried and it does work for me:

PS> $obj1 = New-Object PSObject @{ a = 1; b = 2 }
PS> $obj2 = New-Object PSObject @{ a = 3; b = 4 }
PS> $obj1,$obj2

Name                           Value
----                           -----
a                              1
b                              2
a                              3
b                              4

PS> function write-projects { param ([object[]] $projects) $projects }
PS> write-projects -projects $obj1,$obj2

Name                           Value
----                           -----
a                              1
b                              2
a                              3
b                              4

I know why scenario 3 does not work. It's because when you use Write-Host the object is converted to a text representation using its toString() method and so that's why your object isn't output as it is:

PS> function write-projects { param ([object[]] $projects) write-host $projects }
PS> write-projects -projects $obj1,$obj2
System.Collections.Hashtable System.Collections.Hashtable

# notice the output of toString()
PS> $obj1.ToString()
System.Collections.Hashtable

A better approach is to use Write-Output as (a) this enumerates the objects automatically, and (b) it's "pipeline-friendly" in that the object is not written to the host directly but passed on to the next step.

PS> function write-projects { param ([object[]] $projects) write-output $projects }
PS> write-projects -projects $obj1,$obj2

Name                           Value
----                           -----
a                              1
b                              2
a                              3
b                              4

And the reason why scenario 4 works is because you are outputting the object directly - implicitly enumerating it - and then it's formatted and the formatted output is what Write-Host receives to emit to the screen. I would say you can skip Write-Host in scenario 4 and it should still work.

PS> function write-projects { param ([object[]] $projects) $projects | format-table }
PS> write-projects -projects $obj1,$obj2

Name                           Value
----                           -----
a                              1
b                              2
a                              3
b                              4

Hope that helps!

Upvotes: 2

Start-Automating
Start-Automating

Reputation: 8367

This question has a simple answer and a complex solution:

Don't use Write-Host (except in a formatter)

Write-Host outputs directly to the screen, which prevents you from ever using the data. You ideally want Get-Projects to display the projects the way you want, and still be a real object. You can do this with a formatter.

What's happening when you Write-Host in the first scenario is that PowerShell is coercing the input (a list of objects) into a string.

In the second example, you're running into a nested version of the same bug: Children is being coerced into a string, so that it works for Format-Table.

The correct way to do this would be with a formatter.

There are a few ways to go about making this, but I'd recommend a module I wrote a couple of years ago called EzOut. EZOut lets you use Cmdlets to write formatters, and lets you add them dynamically.

To make this work, modify each output of Get-Projects like so:

$output.pstypenames.clear()
$output.pstypenames.add("Project")

I'd then recommend importing EZOut and writing a custom formatter:

Write-Format -TypeName "Project" -Action {
    $project = $_
    "
    Project Name : $($project.Name)
            ID   : $($project.ID)
            Children $(
                $(
                    $project.Children | format-Table | Out-String | Foreach-Object { "            " + $_ + "
"} 
                )
    "
} |
  Out-FormatData |
  Add-FormatData

This will create a formatter that (should) display the children indented, and will register it.

Upvotes: 0

Related Questions