2

I'm running the Test-AdfsServerHealth (Ref.)

The problem is, one of the output values (value name Output) is an array that shows up as System.Collection.Hashtable and I'm trying to find a way to get this in a neat Excel format.

For instance, this is one of the actual values on the CSV when I export:

Name    Result  Detail  Output
TestServiceAccountProperties    Pass    "" System.Collections.Hashtable

But PowerShell displays:

Name             : TestServiceAccountProperties
Result           : Pass
Detail           :
Output           : {AdfsServiceAccount, AdfsServiceAccountDisabled, AdfsServiceAccountLockedOut,
                   AdfsServiceAccountPwdExpired...}
ExceptionMessage :

The actual command I'm running is:

$ServerResult = Test-AdfsServerHealth
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
mutukrp
  • 97
  • 1
  • 9

2 Answers2

4

tl;dr:

Test-AdfsServerHealth | 
  Select-Object Name, Result, Detail, @{ 
    n='Output'
    e={ $_.prop2.GetEnumerator().ForEach({ '{0}={1}' -f $_.Key, $_.Value }) -join ' ' } 
  } | ExportTo-Csv out.csv

The above serializes each .Output hashtable's entries into single-line string composed of space-separated <key>=<value> pairs (PSv4+ syntax) that should work reasonably well in CSV output.


Since CSV is a text format, PowerShell serializes objects to be exported by calling their .ToString() method.

Complex objects such as [hashtable] instances often yield just their full type name (System.Collections.Hashtable) for .ToString(), which isn't useful in a CSV.

A simplified example (I'm using ConvertTo-Csv, but the example applies analogously to Export-Csv):

# Create a custom object whose .col2 property is a hashtable with 2 
# sample entries and convert it to CSV
PS> [pscustomobject] @{ prop1 = 1; Output = @{ name='foo'; ID=666 } } | ConvertTo-Csv

"prop1","Output"
"1","System.Collections.Hashtable"

If all output objects from Test-AdfsServerHealth had the same hashtable structure in their .Output property, you could try to flatten the hashtable by making its entries columns in their own right, but it sounds like that is not the case.

You must therefore manually transform the hashtable into a text representation that fits into a single CSV column:

You can do this with Select-Object and a calculated property that performs the transformation for you, but you need to decide on a text representation that makes sense in the context of a CSV file.

In the following example, a single-line string composed of space-separated <key>=<value> pairs is created (PSv4+ syntax).

[pscustomobject] @{ prop1 = 1; Output = @{ name='foo'; ID=666 } } | 
  Select-Object prop1, @{ 
    n='Output'
    e={ $_.prop2.GetEnumerator().ForEach({ '{0}={1}' -f $_.Key, $_.Value }) -join ' ' } 
  } | ConvertTo-Csv

For an explanation of the hashtable format that creates the calculated prop2 property, see this answer of mine.

The above yields:

"prop1","prop2"
"1","ID=666 name=foo"

Note, however, that if the values in your hashtables are again complex objects that serialize to their type name only, you'd have to apply the approach recursively.


Optional reading: Flattening a hashtable property into individual columns

If the hashtable-valued properties of the objects to export to a CSV file all have the same structure, you can opt to make the hashtable entries each their own output column.

Let's take the following sample input: a collection of 2 custom objects whose .prop2 value is a hashtable with a uniform set of keys (entries):

$coll = [pscustomobject] @{ prop1 = 1; prop2 = @{ name='foo1'; ID=666 } },
        [pscustomobject] @{ prop1 = 2; prop2 = @{ name='foo2'; ID=667 } }

If you know the key names (of interest) up front, you can simply use an explicit list of calculated properties to create the individual columns:

$coll | select prop1, @{ n='name'; e={ $_.prop2.name } }, @{ n='ID'; e={ $_.prop2.ID } } |
  ConvertTo-Csv

The above yields the following, showing that the hashtable entries became their own columns, name and ID:

"prop1","name","ID"
"1","foo1","666"
"2","foo2","667"

More advanced techniques are required if you do not know the key names up front:

# Create the list of calculated properties dynamically, from the 1st input
# object's .prop2 hashtable.
$propList = foreach ($key in $coll[0].prop2.Keys) {
  # The script block for the calculated property must be created from a 
  # *string* in this case, so we can "bake" the key name into it.
  @{ n=$key; e=[scriptblock]::Create("`$_.prop2.$key") } 
}

$coll | Select-Object (, 'prop1' + $propList) | ConvertTo-Csv

This yields the same output as the previous command with the fixed list of calculated properties.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Great one! I did try with $_.GetEnumerator() method earlier but I think I must have been doing something wrong. I was going to come back here and say "Hey, I've done that before but with no avail" is when I noticed your Tl;DR. Copied the code, adapted a bit more for my requirement and pasted - Voila!! Just what I need and how I need. Thanks kind stranger :) – mutukrp Mar 14 '18 at 14:18
  • @mutukrp: glad it helped; my pleasure. For the sake of completeness I've added a solution for flattening a hashtable into individual columns. – mklement0 Mar 14 '18 at 14:27
1

This won't be significantly difficult, just going to be annoying to do. The reason you are getting "System.collections.hashtable" is because is unable to display everything in that property in a single format like that, there is way to much information. You will have to create another object and put whatever information you want in there.

This prob won't work exactly like you want, but with some tweaking it should get you there.

$ServerResult = Test-ADFSServerHealth
$Object = New-PSObject -Property @{
    'Name' = $ServerResult.name
    'Result' = $ServerResult.Result
    'Detail' = $ServerResult.Detail
    'Output' = ($ServerResult.Output | out-string -stream)
    'ExceptionMessage' = $ServerResult.ExceptionMessage

    }

If your interested, here are the resources I used to find this answer.
Converting hashtable to array of strings
https://devops-collective-inc.gitbooks.io/the-big-book-of-powershell-gotchas/content/manuscript/new-object_psobject_vs_pscustomobject.html

Nick W.
  • 1,536
  • 3
  • 24
  • 40
  • I think I'm nearing a solution. Managed to re-write the code to something like this: ` $ServerResult | select Name, @{Name="OutputResult";Expression={$_.Output | Out-String -Stream}} | Export-csv $outputfilename ` Result truncates some properties within the field: – mutukrp Mar 14 '18 at 13:31
  • @mutukrp: You'll get a _multiline_, padded-to-console-window-width representation with `Out-String`; it's actually nontrivial to convert that into a reasonable single-line representation, so you're better off converting the entries to strings one by one, as in my answer (which also gives you more control over the format). – mklement0 Mar 14 '18 at 13:45
  • As an aside: The use of `Out-String -Stream` actually creates an _array_ of strings in property `Output` here, which then serializes as string `"System.Object[]"` with `Export-Csv`; simply omit `-Stream` to get a single (multiline) string representation. – mklement0 Mar 14 '18 at 13:55