PowerShell: Convert HTML to Plain Text (Tested in 7 and 5.1)

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&nbsp;14 &amp; 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&nbsp;14 &amp; starts at 9:00 AM ET.
AustinDenver
Austin Denver

The tags are gone, but &nbsp; and &amp; 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 &amp; and &nbsp;

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&nbsp;14 &amp; 15, 2026 &lt;draft&gt; &quot;Denver&quot; &#36;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
PowerShell WebUtility HtmlDecode with a no-break space
HtmlDecode turns &nbsp; into a no-break space, not a normal one (PowerShell 7)

Watch the &nbsp;. 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 &amp; 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 like extracting text between delimiters.

Keep line breaks from paragraphs and lists

Stripping every tag also throws away the structure. Turn <br> and closing block tags into newlines first, and list items into dashes:

$html = &#