To convert HTML to plain text in PowerShell, strip the tags with -replace and then decode entities like & with WebUtility.HtmlDecode. For a short snippet, this one line does both:
[System.Net.WebUtility]::HtmlDecode(($html -replace '<[^>]+>', ''))
Full pages and email bodies need a bit more care, though. Scripts, styles and line breaks get in the way, and Windows PowerShell 5.1 and PowerShell 7 don’t agree on a few of the tools. I ran everything below in both.
Strip HTML tags with -replace
The pattern <[^>]+> matches a less-than sign, everything that isn’t a greater-than sign, and the closing bracket. In other words, one tag. Replace each match with nothing:
$html = '<p>The <b>Q4 budget</b> review is on Oct 14 & starts at 9:00 AM ET.</p>'
$html -replace '<[^>]+>', ''
$cells = '<td>Austin</td><td>Denver</td>'
$cells -replace '<[^>]+>', ''
($cells -replace '<[^>]+>', ' ' -replace '\s+', ' ').Trim()
Output:
The Q4 budget review is on Oct 14 & starts at 9:00 AM ET.
AustinDenver
Austin Denver
The tags are gone, but and & are still there. And the table cells ran together as AustinDenver, so replace tags with a space when they separate words, then collapse the spaces.
-replace swaps every match, not just the first. The pattern syntax is covered in my PowerShell regex guide.
Decode HTML entities like & and
WebUtility.HtmlDecode turns entities back into characters. It’s part of System.Net, so it works in PowerShell 7 and 5.1 with no setup:
$text = 'Oct 14 & 15, 2026 <draft> "Denver" $1,250'
$plain = [System.Net.WebUtility]::HtmlDecode($text)
$plain
'No-break spaces: {0}' -f [regex]::Matches($plain, '\u00A0').Count
$plain = $plain -replace '\u00A0', ' '
'After the fix: {0}' -f [regex]::Matches($plain, '\u00A0').Count
Output:
Oct 14 & 15, 2026 <draft> "Denver" $1,250
No-break spaces: 1
After the fix: 0

Watch the . It becomes U+00A0, a no-break space that looks like a space but fails a match on ‘ ‘. Swap it for a regular space, the same way you’d replace any text in a string.
Many scripts use System.Web.HttpUtility instead. That type isn’t loaded in Windows PowerShell 5.1:
try {
[System.Web.HttpUtility]::HtmlDecode('Tom & Maria')
} catch {
"Failed. $($_.Exception.Message)"
}
Output:
Tom & Maria
That’s PowerShell 7. Windows PowerShell 5.1 prints Failed. Unable to find type [System.Web.HttpUtility].
In 5.1, run Add-Type -AssemblyName System.Web first, or just use WebUtility. The try and catch blocks keep the error readable.
Remove script and style blocks first
My sample file, update.html, is a small office notice with a style block in the head and a script at the bottom. A plain tag strip keeps the code inside them:
$html = Get-Content -Path 'C:\psfaqs\htmltext\update.html' -Raw
$naive = $html -replace '<[^>]+>', ''
$clean = $html -replace '(?is)<(script|style)\b[^>]*>.*?</\1>', '' -replace '<[^>]+>', ''
'Naive text has CSS: {0}' -f ($naive -match 'color:')
'Naive text has JavaScript: {0}' -f ($naive -match 'console\.log')
'Clean text has either: {0}' -f ($clean -match 'color:|console\.log')
Output:
Naive text has CSS: True
Naive text has JavaScript: True
Clean text has either: False
The (?is) prefix makes the match case-insensitive and lets the dot cross line breaks, so .*? spans the whole block. The \1 makes the closing tag match the opening one, much l