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

# Default: an ARRAY of strings, one element per line Get-Content .\log.txt (Get-Content .\log.txt).Count # the number of lines # -Raw: a SINGLE string containing the whole file, newlines and all Get-Content .\log.txt -Raw (Get-Content .\log.txt -Raw).Count # 1 — it's one string, not one-per-line anymore (Get-Content .\log.txt -Raw).Length # total character count instead

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

"Line 1" | Set-Content .\out.txt # overwrites the file "Line 2" | Add-Content .\out.txt # appends instead of overwriting # Out-File captures the same FORMATTED text you'd see on screen — not the raw objects Get-Process | Out-File .\procs.txt # a display-formatted table, subject to console width

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 -AutoSize Get-Process | Format-List *
Piping Format-Table into anything else silently breaks
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.
The central fact this chapter is built on
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

Get-Process | Select-Object Name, CPU -First 5 | Export-Csv .\procs.csv -NoTypeInformation $reloaded = Import-Csv .\procs.csv $reloaded[0].CPU.GetType().Name # String — NOT Double anymore!

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

$data = Get-Process | Select-Object Name, CPU -First 3 $json = $data | ConvertTo-Json $json | ConvertFrom-Json # real objects again — PSCustomObject, close to the originals # Default -Depth is only 2 — nested objects past that silently flatten to a plain string $deeplyNested | ConvertTo-Json -Depth 10

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.

A first practical habit
Reach for 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

Exercise 1

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.

📄 View solution
Exercise 2

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.

📄 View solution
Exercise 3

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.

📄 View solution

Chapter 8 Quick Reference

  • Get-Content — array of lines by default; -Raw gives one whole-file string instead, changing what .Count means
  • Set-Content / Add-Content — write exact string content; overwrite vs. append
  • Out-File — captures the same display-formatted text the console would show, not a clean data dump
  • Format-Table / Format-List — display only; converts objects into non-queryable formatting instructions — must be the last thing in a pipeline
  • Export-Csv / Import-Csv — real structured text, but every value round-trips back as a plain [string], even former numbers
  • ConvertTo-Json / ConvertFrom-Json — real structured interchange; default -Depth 2 silently 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