skip to content

In PowerShell, `Get-Process | Format-Table Name, CPU | Export-Csv procs.csv` writes a CSV of formatting metadata instead of process data. Why are the `Format-*` cmdlets the end of a pipeline?

level: middleimportance: should knowfreq 45%

answer

  1. formatting replaces your objects
  2. display instructions, not data
  3. only Out-* may follow
  4. Select-Object shapes, Format-* paints
  5. the console table is a view, not the data

basics

~20 s

Format-Table does not reshape your data, it replaces it with internal formatting-instruction objects that only the output cmdlets understand. Anything placed after a Format-* cmdlet therefore receives display instructions, not process objects. Format for humans last; use Select-Object or Export-Csv for data.

solid answer

~40 s

`Format-Table`, `Format-List`, `Format-Wide` and `Format-Custom` are renderers, not projections. They consume your objects and emit a stream of internal formatting-instruction objects from the `Microsoft.PowerShell.Commands.Internal.Format` namespace, which `Out-Host`, `Out-File` and the default output path know how to turn into text. `Export-Csv` does not: it faithfully serialises the properties of whatever it is given, so you get the instruction objects' own members as columns and no `Name` or `CPU` anywhere. The rule is that a `Format-*` cmdlet may only be followed by an `Out-*` cmdlet, and it must be the last stage that touches data. If you want fewer properties in a file, use `Select-Object Name, CPU | Export-Csv`; if you genuinely want the rendered text, `Out-String` or `Out-File` after the format cmdlet is the honest way to get it.

code

powershell · 8 lines
powershell
# Broken: Export-Csv serialises formatting instructions
Get-Process | Format-Table Name, CPU | Export-Csv .\procs.csv

# Correct: shape the data, then serialise it
Get-Process | Select-Object Name, Id, CPU | Export-Csv .\procs.csv -NoTypeInformation

# Proof of the substitution
Get-Process | Format-Table Name, CPU | Get-Member

go deeper

for a junior

Remember the rule: Format-* goes last and nothing but an Out-* cmdlet may follow it. To write data to a file, use Select-Object then Export-Csv.

for a middle

Explain the mechanism — format cmdlets replace your objects with internal display-instruction objects that only the output cmdlets interpret — and show it with Get-Member after a Format-Table.

for a senior

Separate shaping from rendering when reviewing scripts, and recognise the silent-corruption shape of this bug: no error is raised, so a scheduled export can produce useless files for weeks.

for a principal

Set the convention that anything machine-consumed is serialised from real objects, never scraped from rendered output, and that rendered text is a human artefact with no contract behind it.

## What Format-* actually emits The mental model most people carry is that `Format-Table Name, CPU` gives you "the same objects, with fewer columns." It does not. `Format-Table` is a rendering engine front end. It consumes your objects and emits a *different* stream: a sequence of internal formatting-instruction objects describing a table — where it starts, what its columns are, one entry per row, where it ends. Their types live in the `Microsoft.PowerShell.Commands.Internal.Format` namespace, and they exist to be interpreted by an output cmdlet. You can see the substitution directly: ```powershell Get-Process | Get-Member # System.Diagnostics.Process Get-Process | Format-Table Name, CPU | Get-Member # Format-namespace types instead ``` That second line is the whole answer to the question. By the time the pipeline reaches `Export-Csv`, there is no process object left to serialise. ## Why Export-Csv produces nonsense `Export-Csv` is a faithful serialiser: it takes each object, enumerates its properties, and writes them as columns. Fed formatting-instruction objects, it does exactly that — it writes *their* internal members as the header row and their values as data. The result is a valid CSV of something you never wanted, with no `Name` or `CPU` column and usually a row count that does not match your process count, because table start, group start and table end are objects too. Nothing errors; you just get garbage, which is why this bites people in scripts that appear to work. The same applies to `ConvertTo-Json`, `Export-Clixml`, `Where-Object`, `Sort-Object` and `Select-Object`: put any of them after a `Format-*` cmdlet and they operate on display instructions. ## The rule > A `Format-*` cmdlet may be followed only by an `Out-*` cmdlet, and by nothing that treats the stream as data. `Out-Host` (the implicit destination of anything falling off the end of a pipeline), `Out-File` and `Out-String` all understand formatting instructions and turn them into text. That pairing is legitimate: ```powershell Get-Process | Format-Table Name, CPU -AutoSize | Out-File .\procs.txt ``` That file contains a human-readable table — the same thing you would see on screen, including column padding and truncation. It is fine for a report a person reads and useless as an input to another program. ## Select-Object is the data-shaping cmdlet The fix is to separate two jobs that look similar: - **Choosing which properties you care about** is `Select-Object`. It emits real objects (with the chosen properties) that downstream cmdlets can still filter, sort and serialise. - **Deciding how those properties are painted on a screen** is `Format-*`, and it is the last thing that happens. ```powershell # Data out to a file Get-Process | Select-Object Name, Id, CPU | Export-Csv .\procs.csv -NoTypeInformation # Same data, rendered for a human, last Get-Process | Select-Object Name, Id, CPU | Sort-Object CPU -Descending | Format-Table -AutoSize ``` Note the ordering in the second line: every stage that reasons about values comes first, and formatting is appended at the very end. ## The related surprise: implicit formatting Even when you never type a `Format-*` cmdlet, one runs. Objects that fall off the end of a pipeline go to the default output path, which consults the type's registered formatting data to decide the layout — which is why `Get-Process` shows a handful of columns out of dozens of properties, and why an object with more than four properties tends to render as a list rather than a table. The console view is a *view*. `Get-Member`, `Select-Object *` and `Format-List *` are how you see past it. This also explains a related trap: `Get-Process | Out-File procs.txt` writes the rendered table, not the data, for exactly the same reason — `Out-File` sits at the end of the output path and receives formatted text. ## What to say in an interview Name the mechanism (format cmdlets substitute display-instruction objects for your data), name the rule (`Format-*` last, only `Out-*` after it), and name the correct alternative (`Select-Object` to shape data, `Export-Csv`/`ConvertTo-Json` to serialise it). Mentioning that the console table is itself produced by that same formatting layer shows you understand it is not an edge case but the normal end of every pipeline.

  • If Format-Table is only for display, why does `Get-Process` still print a neat table when you type it alone?
    Because the default output path formats for you. Objects that fall off the end of a pipeline are handed to the formatting layer, which consults the registered formatting data for their type and picks a table or list plus a default set of columns. You never typed `Format-Table`, but one ran on your behalf.
  • What is the right way to capture a rendered table as text rather than as data?
    Follow the format cmdlet with `Out-String` if you want a string in the session, or `Out-File` if you want it on disk. Both understand the formatting instructions and produce the same text you would see on screen. Just be clear that you are producing a human report — the column widths and truncation make it a poor machine input.
  • Is `Select-Object Name, CPU` interchangeable with `Format-Table Name, CPU`?
    Only in what appears on screen. `Select-Object` emits real objects carrying those properties, so you can keep filtering, sorting and exporting afterwards. `Format-Table` emits display instructions and ends the data pipeline. If anything downstream needs the values, it must be `Select-Object`.

saying these in an interview costs you the question

  • Believes Format-Table returns objects with fewer properties
  • Puts Sort-Object or Where-Object after a Format-* cmdlet
  • Says Export-Csv is broken rather than mis-fed
  • Thinks the console table is the object's real shape
  • Uses Out-File on objects and expects parseable data

context