Skip to content

Reading & Writing Files

Note

Read and write text file contents with Get-Content, Set-Content, Add-Content, and Out-File — and know when to reach for each one.

Overview

PowerShell has four core cmdlets for working with file contents, and they overlap just enough to be confusing at first. Get-Content reads. Set-Content and Out-File both write and overwrite. Add-Content appends. The difference that actually matters day to day is encoding defaults and pipeline behavior — get those right and the rest is straightforward.

Basic Syntax

Get-Content -Path "C:\Temp\file.txt"
Set-Content -Path "C:\Temp\file.txt" -Value "New content"
Add-Content -Path "C:\Temp\file.txt" -Value "Appended line"
Get-Process | Out-File -Path "C:\Temp\processes.txt"

Key Points

  • Get-Content returns an array of strings (one per line) by default
  • Set-Content and Out-File both overwrite the file — use Add-Content to append
  • Set-Content writes strings as-is; Out-File formats objects the way the console would display them
  • Always specify -Encoding explicitly when it matters — defaults have changed between PowerShell versions

Reading Files

Get-Content Basics

# Read entire file as an array of lines
$lines = Get-Content -Path "C:\Temp\log.txt"
$lines.Count

# Read as a single string instead of an array
$text = Get-Content -Path "C:\Temp\log.txt" -Raw

# Read only the first N lines
Get-Content -Path "C:\Temp\log.txt" -TotalCount 10

# Read only the last N lines (tail)
Get-Content -Path "C:\Temp\log.txt" -Tail 20

-Raw vs Default Array Output

Get-Content without -Raw returns one string per line, which is what you want for foreach loops or Select-String-style filtering. -Raw returns the whole file as a single string, which is what you want for ConvertFrom-Json, regex across line breaks, or anything that treats the file as one blob of text.

Live Tail (Follow a Growing File)

# Watch a log file as new lines are written, like tail -f
Get-Content -Path "C:\Logs\app.log" -Tail 10 -Wait

Filtering While Reading

# Only lines containing "ERROR"
Get-Content -Path "C:\Logs\app.log" | Where-Object { $_ -match "ERROR" }

# Skip the header row of a data file
Get-Content -Path "C:\Data\export.txt" | Select-Object -Skip 1

Writing Files

Set-Content — Overwrite

# Overwrite with a single line
Set-Content -Path "C:\Temp\output.txt" -Value "Report generated $(Get-Date)"

# Overwrite with multiple lines (array)
$lines = @("Line 1", "Line 2", "Line 3")
Set-Content -Path "C:\Temp\output.txt" -Value $lines

# Specify encoding explicitly
Set-Content -Path "C:\Temp\output.txt" -Value "Data" -Encoding UTF8

Set-Content Always Overwrites

# BAD - This replaces the entire file every time it runs
Set-Content -Path "C:\Logs\app.log" -Value "New entry"

# GOOD - Use Add-Content if you want to append instead
Add-Content -Path "C:\Logs\app.log" -Value "New entry"
There's no accumulation with Set-Content — each call replaces the previous contents completely.

Add-Content — Append

# Append a single line
Add-Content -Path "C:\Logs\app.log" -Value "$(Get-Date) - Script started"

# Append multiple lines
Add-Content -Path "C:\Logs\app.log" -Value @("Line A", "Line B")

# Common logging pattern
function Write-Log {
    param([string]$Message)
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    Add-Content -Path "C:\Logs\script.log" -Value "[$timestamp] $Message"
}

Out-File — Redirect Command Output

# Redirect formatted output (like the console would show it) to a file
Get-Process | Out-File -Path "C:\Temp\processes.txt"

# Append instead of overwrite
Get-Date | Out-File -Path "C:\Temp\log.txt" -Append

# Widen the output so table columns don't get truncated
Get-Process | Out-File -Path "C:\Temp\processes.txt" -Width 200

Out-File vs Set-Content for Objects

Out-File runs objects through PowerShell's default formatter first — the same thing that happens when you print to the console. That's great for human-readable reports, but it means Out-File output is text-formatted, not structured. If you need the raw data back later, use Export-Csv or ConvertTo-Json, not Out-File.

Working with CSV, JSON, and XML

# CSV round-trip
$data = Import-Csv -Path "C:\Data\users.csv"
$data | Export-Csv -Path "C:\Data\users-copy.csv" -NoTypeInformation

# JSON round-trip
$config = Get-Content -Path "C:\Config\settings.json" -Raw | ConvertFrom-Json
$config | ConvertTo-Json -Depth 5 | Set-Content -Path "C:\Config\settings.json"

# XML round-trip
[xml]$xmlContent = Get-Content -Path "C:\Data\config.xml" -Raw
$xmlContent.Save("C:\Data\config-copy.xml")

JSON Needs -Raw

ConvertFrom-Json expects a single string, not an array of lines. Always pipe Get-Content -Raw into it — without -Raw, multi-line JSON gets split into separate strings and conversion fails or silently returns wrong results. See Working with JSON for the full reference.

Important Parameters

Parameter Type Description Example
-Path String File to read or write Get-Content -Path "C:\a.txt"
-Raw Switch Get-Content: return one string, not an array Get-Content -Raw
-Tail Int Get-Content: return only the last N lines Get-Content -Tail 20
-Wait Switch Get-Content: keep watching the file for new lines Get-Content -Tail 5 -Wait
-Value Object Set/Add-Content: content to write Set-Content -Value "text"
-Append Switch Out-File: append instead of overwrite Out-File -Append
-Encoding String Character encoding (UTF8, ASCII, Unicode) Set-Content -Encoding UTF8
-Force Switch Overwrite read-only files Set-Content -Force

Common Patterns

# Pattern 1: Read, transform, write back
$content = Get-Content -Path "C:\Data\input.txt"
$transformed = $content | ForEach-Object { $_.ToUpper() }
Set-Content -Path "C:\Data\output.txt" -Value $transformed

# Pattern 2: Simple logging function
function Write-Log {
    param([string]$Message, [string]$Path = "C:\Logs\script.log")
    "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message" | Add-Content -Path $Path
}

# Pattern 3: Read a config file safely with a default
$configPath = "C:\Config\settings.json"
$config = if (Test-Path $configPath) {
    Get-Content -Path $configPath -Raw | ConvertFrom-Json
} else {
    [PSCustomObject]@{ RetryCount = 3; Timeout = 30 }
}

Real-World Examples

Example: Rotating Error Log Extractor

Scenario: Pull every "ERROR" line from today's log into a separate summary file, tagged with a timestamp.

$logPath = "C:\Logs\app.log"
$summaryPath = "C:\Logs\errors-summary.txt"

$errors = Get-Content -Path $logPath | Where-Object { $_ -match "ERROR" }

if ($errors) {
    $header = "=== Errors extracted $(Get-Date) - $($errors.Count) found ==="
    Add-Content -Path $summaryPath -Value $header
    Add-Content -Path $summaryPath -Value $errors
}

Explanation: Get-Content reads line-by-line so Where-Object -match can filter per line. Add-Content appends rather than overwrites, so the summary file accumulates history across runs instead of losing it.

Tips & Tricks

Reading Large Files

# BAD - loads the entire file into memory at once
$lines = Get-Content -Path "C:\Logs\huge.log"

# BETTER - stream line by line without holding it all in memory
Get-Content -Path "C:\Logs\huge.log" | ForEach-Object {
    # process $_ one line at a time
}
Piping Get-Content directly into ForEach-Object processes the file as a stream instead of materializing the whole array first — much lighter on memory for multi-gigabyte logs.

Encoding Mismatches

Writing a file with -Encoding UTF8 in Windows PowerShell 5.1 adds a byte-order mark (BOM) that some tools choke on; PowerShell 7+ writes UTF-8 without a BOM by default. If a downstream tool complains about the first few bytes of a file PowerShell generated, check -Encoding explicitly rather than assuming defaults match across versions.

Additional Resources