DEV Community

Cover image for PowerShell Select-Object vs Format-Table: The Real Difference (and Why Your CSV Is Full of GUIDs)
arnostorg
arnostorg

Posted on Edited on

PowerShell Select-Object vs Format-Table: The Real Difference (and Why Your CSV Is Full of GUIDs)

Select-Object and Format-Table both let you pick which columns you see, and in the console they look almost identical. Then you pipe the result into Export-Csv and get a file full of GUIDs instead of file names. That is not a bug in Export-Csv. It is the one real difference between these two cmdlets, and once you see it you stop making the mistake.

Everything below was run in PowerShell 7.4.6. The behavior is the same in Windows PowerShell 5.1.

The shortest honest comparison

Select-Object Format-Table
What it outputs Real objects with the properties you picked Formatting instructions for the host
Can you sort, filter, or export afterwards? Yes No, not in any useful way
Where it belongs in a pipeline Anywhere Last, or right before Out-File / Out-String
Typical job Shape data for the next command Make data readable for a human

If you remember one rule, remember the one Microsoft Learn calls format right: formatting cmdlets go at the very end of the line.

The bug: Format-Table before Export-Csv

Here is the mistake almost everyone makes once:

Get-ChildItem C:\Logs\*.txt |
    Format-Table Name, Length |
    Export-Csv C:\Reports\logs.csv -NoTypeInformation
Enter fullscreen mode Exit fullscreen mode

And here is what actually lands in the CSV:

"ClassId2e4f51ef21dd47e99d3c952918aff9cd","pageHeaderEntry","pageFooterEntry","autosizeInfo","shapeInfo","groupingEntry"
"033ecb2bc07a4d43b5ef94ed5a35d280",,,,"Microsoft.PowerShell.Commands.Internal.Format.TableHeaderInfo",
"9e210fe47d09416682b841769c78b8a3",,,,,
"27c87ef9bbda4f709f6b4002fa4af63c",,,,,
Enter fullscreen mode Exit fullscreen mode

No file names, no sizes. And Export-Csv is not wrong to stay quiet. As Mark Wragg pointed out in the comments, Format-Table still outputs real PowerShell objects (FormatStartData, GroupStartData, FormatEntryData, and so on). Export-Csv is built to serialize whatever objects it receives, so it writes the properties of those formatting records instead of your data. That is why Microsoft handles it with a warning in the Export-Csv documentation and not a blocked command: "Do not format objects before sending them to the Export-Csv cmdlet. If Export-Csv receives formatted objects the CSV file contains the format properties rather than the object properties."

The fix is one word:

Get-ChildItem C:\Logs\*.txt |
    Select-Object Name, Length |
    Export-Csv C:\Reports\logs.csv -NoTypeInformation
Enter fullscreen mode Exit fullscreen mode
"Name","Length"
"log1.txt","101"
"log2.txt","201"
"log3.txt","301"
Enter fullscreen mode Exit fullscreen mode

-NoTypeInformation is only needed in Windows PowerShell 5.1. PowerShell 7 leaves the #TYPE header out by default, and the switch is harmless there.

Prove it with Get-Member

You do not have to take anyone's word for it. Ask PowerShell what type comes out of each cmdlet:

Get-ChildItem *.txt | Format-Table Name, Length | Get-Member | Select-Object -First 1 -ExpandProperty TypeName
# Microsoft.PowerShell.Commands.Internal.Format.FormatStartData

Get-ChildItem *.txt | Select-Object Name, Length | Get-Member | Select-Object -First 1 -ExpandProperty TypeName
# Selected.System.IO.FileInfo
Enter fullscreen mode Exit fullscreen mode

Select-Object gives you new objects that still carry Name and Length as real properties. Format-Table gives you internal formatting records that only the console knows how to draw.

The quieter bug: sorting and filtering after Format-Table

The CSV case is loud. These two are silent, which makes them worse.

# Looks fine, sorts nothing
Get-ChildItem *.txt | Format-Table Name, Length | Sort-Object Length -Descending
Enter fullscreen mode Exit fullscreen mode

There is no error. The table prints in the original order, because Sort-Object is sorting formatting records that have no Length property.

(Get-ChildItem *.txt | Format-Table Name, Length | Where-Object Length -gt 200 | Measure-Object).Count
# 0

(Get-ChildItem *.txt | Select-Object Name, Length | Where-Object Length -gt 200 | Measure-Object).Count
# 4
Enter fullscreen mode Exit fullscreen mode

Same input, same filter, completely different answer. If a script "finds nothing" and there is a Format-* anywhere before the filter, that is your first suspect.

The right order: filter, sort, select, then format

Get-ChildItem C:\Logs -File |
    Where-Object Length -gt 1MB |
    Sort-Object Length -Descending |
    Select-Object -First 10 Name, Length, LastWriteTime |
    Format-Table -AutoSize
Enter fullscreen mode Exit fullscreen mode

Read it left to right: narrow the data, order it, keep the columns you need, and only then make it pretty. Swap the last line for Export-Csv, ConvertTo-Json, or another cmdlet and the pipeline still works, because everything before it is real data.

Table on screen and CSV in one pipeline: Tee-Object

Want to see the table and save the CSV in one go? Mark Wragg suggested Tee-Object, which sends output in two directions: it stores it in a file or variable and also passes it down the pipeline. Use -Variable, not -FilePath:

Get-ChildItem C:\Logs\*.txt |
    Select-Object Name, Length |
    Tee-Object -Variable rows |
    Format-Table -AutoSize

$rows | Export-Csv C:\Reports\logs.csv -NoTypeInformation
Enter fullscreen mode Exit fullscreen mode
Name     Length
----     ------
log1.txt    101
log2.txt    201
log3.txt    301
Enter fullscreen mode Exit fullscreen mode
"Name","Length"
"log1.txt","101"
"log2.txt","201"
"log3.txt","301"
Enter fullscreen mode Exit fullscreen mode

Why not Tee-Object -FilePath logs.csv? Internally Tee-Object uses Out-File for the file, so the file gets the rendered table text, not comma-separated values. Real objects stay in $rows, and Export-Csv turns them into a proper CSV.

Calculated columns belong in Select-Object too

Both cmdlets accept calculated properties, so it is tempting to build a "SizeKB" column inside Format-Table. Build it in Select-Object instead and it survives the export:

Get-ChildItem C:\Logs\*.txt |
    Select-Object Name, @{Name = 'SizeKB'; Expression = { [math]::Round($_.Length / 1KB, 2) }} |
    Export-Csv C:\Reports\logs-kb.csv -NoTypeInformation
Enter fullscreen mode Exit fullscreen mode
"Name","SizeKB"
"log1.txt","0.1"
"log2.txt","0.2"
"log3.txt","0.29"
Enter fullscreen mode Exit fullscreen mode

When Format-Table is exactly right

Format-Table is not the villain. It is the right tool whenever a human is the last consumer:

# Readable console output
Get-Service | Where-Object Status -eq 'Running' | Format-Table Name, DisplayName -AutoSize

# Readable text report for a ticket or email
Get-ChildItem C:\Logs -File | Format-Table Name, Length -AutoSize | Out-File C:\Reports\logs.txt
Enter fullscreen mode Exit fullscreen mode

Out-File and Out-String understand formatting records, so a formatted table written to a .txt file looks exactly like it did on screen. See the Format-Table reference for -AutoSize, -Wrap, and -GroupBy.

Two Select-Object switches worth knowing

From the Select-Object reference:

# Just the values, not objects with one property
Get-ChildItem C:\Logs\*.txt | Select-Object -ExpandProperty Name

# Top N after sorting
Get-ChildItem C:\Logs -File | Sort-Object Length -Descending | Select-Object -First 5 Name, Length
Enter fullscreen mode Exit fullscreen mode

-ExpandProperty is the one to reach for when the next command wants plain strings, like a list of names to feed into Remove-Item or Copy-Item.

Quick checklist

  • Next step is a cmdlet, a file format, or another script: use Select-Object.
  • Next step is your eyes or a .txt report: use Format-Table, and put it last.
  • Export full of GUIDs, sort that does nothing, or filter that returns zero: look for a Format-* too early in the pipeline.
  • Need a computed column you can export: build it in Select-Object with a calculated property.

Practice the pipeline order hands-on

Reading about pipeline order is easy; getting it right under pressure takes reps. CMD Master's free interactive PowerShell, CMD, and Bash practice in your browser lets you run real commands in a browser terminal with no install, so you can drill filter-sort-select-format until it is automatic.

What is the strangest thing you have found in a CSV that went through Format-Table first?

Top comments (10)

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

format last, select first. that GUID-filled CSV is a brutal way to find out.

Collapse
 
arnostorg profile image
arnostorg •

Yeah, and the worst part is that Export-Csv doesn't even complain, it just writes the formatting records. If you want the table on screen and the CSV both, put the Select-Object result in a variable and pipe that same variable to Export-Csv and to Format-Table. Microsoft warns about this in the Export-Csv docs too.

Collapse
 
markwragg profile image
Mark Wragg •

To be fair, the output of Format-Table is still a PowerShell Object, and Export-CSV is designed to consume objects, so it has nothing to really complain about, and arguably shouldn't be second guessing you (even though we know in reality there's unlikely to be a practical reason to export the Format objects).

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

That framing helps. Export-Csv doing its job quietly is what lets the formatting records slip through unnoticed.

Thread Thread
 
arnostorg profile image
arnostorg •

@markwragg Fair point, Export-Csv is doing exactly its job here. Format-Table emits real objects, they're just formatting records, so it faithfully serializes their properties. That's probably why Microsoft handles it with a warning in the docs ('Do not format objects before sending them to the Export-Csv cmdlet') rather than the cmdlet blocking it: learn.microsoft.com/en-us/powershe...

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

That silent part is the worst bit. Storing the Select-Object result once and piping it both ways keeps the file clean and the screen readable.

Thread Thread
 
markwragg profile image
Mark Wragg •

There’s a cmdlet for exactly that fyi: Tee-Object

Thread Thread
 
robertpaschal profile image
Odinaka Robert Nnamani •

Tee-Object beats storing it first. One pipe, both directions, nothing extra to keep track of.

Thread Thread
 
arnostorg profile image
arnostorg •

@markwragg Good shout, Tee-Object fits this nicely. One thing I hit while testing: -FilePath saves the rendered table text, not CSV, so here it's ... | Select-Object Name, Id | Tee-Object -Variable rows | Format-Table, then $rows | Export-Csv out.csv. learn.microsoft.com/en-us/powershe...

Thread Thread
 
arnostorg profile image
arnostorg •

@markwragg Thanks again for both points. I've updated the article: it now explains that Format-Table still emits real objects, so Export-Csv is doing its job and Microsoft handles it with a docs warning, and there's a new Tee-Object section showing the table on screen plus a clean CSV via -Variable. Much better post because of it.