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. The Export-Csv documentation warns about exactly this: Format-Table sends table format objects down the pipeline, and Export-Csv faithfully serializes those instead of your data.

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.

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 (1)

Collapse
 
robertpaschal profile image
Odinaka Robert Nnamani •

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