Working with Files, Text & Formatting Output
PowerShell Fundamentals
Chapter 8 · Working with Files, Text & Formatting Output
Reading and writing plain files is simple enough on its own — the real substance of this chapter is what happens at the two edges of the object pipeline. On one edge, Format-Table/Format-List exist purely to make objects readable on screen, and doing so deliberately breaks Chapter 1's own object pipeline on purpose. On the other edge, ConvertTo-Json/ConvertTo-Csv do the opposite job: turning real objects into genuine structured text that something else — a file, another program, a REST API — can consume and, mostly, reconstruct. Confusing the two is one of the most common mistakes in real-world PowerShell scripts.
Reading Files: Get-Content, and the -Raw Gotcha
Without -Raw, .Count tells you how many lines the file has — exactly what you want for foreach-ing line by line. With -Raw, .Count is always 1, because there's only one (much longer) string — what you want instead whenever a whole-file regex match (-match, Chapter 4) needs to see line breaks as part of the text it's searching, not as separators between array elements.
Writing Files: Set-Content, Add-Content & Out-File
Set-Content and Add-Content write exactly the string content you give them. Out-File is different in a way that trips people up: piping objects straight into it captures whatever the console would have displayed — including the same automatic column formatting a wide table gets truncated to on screen — not a clean, structured dump of the underlying data.
Format-Table & Format-List: Display Only, Never Pipe Further
Get-Process | Format-Table Name, CPU | Where-Object { $_.CPU -gt 10 } doesn't filter correctly — because by the time Where-Object receives anything, it's no longer receiving process objects at all. Format-Table/Format-List convert the pipeline into special internal formatting instruction objects meant only for a screen or a text file, and the real CPU/Name properties Chapter 3's Get-Member would have found on a process object simply aren't there anymore. The rule: a Format-* cmdlet should always be the last thing in a pipeline, followed only by Out-Host, Out-File, or nothing at all — filter and sort before formatting, never after.
Format-* and ConvertTo-* look similar — both take real objects and turn them into text — but they exist for opposite purposes. Format-Table/Format-List are a deliberate, permanent exit from the object pipeline: display formatting for a human, with no path back. ConvertTo-Json/ConvertTo-Csv are a bridge: real, structured interchange formats designed specifically to be read back in — by ConvertFrom-Json, by Import-Csv, or by an entirely different program on the other end of an API call. Ask "is a human reading this on screen right now" versus "does this data need to leave PowerShell and come back later" before reaching for either family.
ConvertTo-Csv / Export-Csv: Real Structured Text
CSV is plain text, and plain text has no concept of a number versus a string — every value that survives a round trip through Export-Csv/Import-Csv comes back as a plain [string], even a column that started out as a real [double]. Comparing a re-imported value with -gt 10 (Chapter 4) can behave unexpectedly for exactly this reason unless you explicitly cast it back — [double]$reloaded[0].CPU -gt 10 — first.
ConvertTo-Json / ConvertFrom-Json: and the Depth Gotcha
ConvertTo-Json's default depth of 2 is easy to hit without noticing — a nested object more than two levels deep gets silently collapsed into a string like "System.Object[]" instead of real, readable JSON. Any time nested data seems to have "gone missing" after converting to JSON, check -Depth before assuming the data was never there.
ConvertTo-Json as a genuinely useful debugging tool even when you're not building any kind of API — $someObject | ConvertTo-Json -Depth 5 shows you exactly what's really inside a complex object, in a readable structured form, faster than reading raw Get-Member output.
Hands-On Exercises
Explain why Get-Process | Format-Table Name, CPU | Where-Object { $_.CPU -gt 10 } fails to filter correctly, using this chapter's own explanation of what Format-Table actually produces.
Explain the difference between Get-Content file.txt and Get-Content file.txt -Raw. Describe a situation where using the wrong one would give you an incorrect line count or an incorrect character count.
You export process data with Export-Csv, then read it back with Import-Csv. Explain why comparing a re-imported CPU value with -gt 10 might not behave the way you expect, and how you'd fix it.
Chapter 8 Quick Reference
Get-Content— array of lines by default;-Rawgives one whole-file string instead, changing what.CountmeansSet-Content/Add-Content— write exact string content; overwrite vs. appendOut-File— captures the same display-formatted text the console would show, not a clean data dumpFormat-Table/Format-List— display only; converts objects into non-queryable formatting instructions — must be the last thing in a pipelineExport-Csv/Import-Csv— real structured text, but every value round-trips back as a plain[string], even former numbersConvertTo-Json/ConvertFrom-Json— real structured interchange; default-Depth 2silently flattens deeper nested data- This chapter's own throughline:
Format-*is a dead end for display;ConvertTo-*is a bridge back to real data - Next chapter: Error Handling & Basic Debugging