Regex Capture-Group Replacement in PowerShell (for .NET Developers)

Aerial photo of a dam.

PowerShell runs on .NET and uses the .NET regex engine, so its regular expressions are familiar to .NET developers. Still, PowerShell adds its own string semantics. I ran into two independent failures while extracting article content from HTML: the pattern did not match across multiple lines, and PowerShell interpolated $1 before the replacement reached the regex engine. My .NET knowledge helped with the first problem but hid the second.

TL;DR

When you use literal regex patterns and replacement strings that do not require interpolation, put them in single-quoted strings. Also choose the regex options that match your input.

Let's create some data

Anything is better with an example, so let's use PowerShell to download a blog and extract the article content using a regular expression. First, we'll download a blog into a string, like this:

$ErrorActionPreference = "Stop"

# download article
$domain = "https://keestalkstech.com"
$url = "$domain/2020/05/plotting-a-grid-of-pil-images-in-jupyter/"
$response = Invoke-WebRequest $url
$article = $response.Content

The -UseBasicParsing parameter is deprecated and has been a no-op since PowerShell 6. It is only relevant to Windows PowerShell 5.1 behavior.

Fail on the first try

My first attempt was the following code:

$article = $article -replace ".*<article.*?>\s*(.*)\s*</article>.*", "$1"

PowerShell accepts it without an error. It looks good to me as a .NET developer... but it does not do anything! I end up with exactly the same string because . does not match the newline characters in the article.

Regular expression options: (?s)

First, we need to fix multiline matching. The (?s) Singleline mode option makes . match every character, including newline characters. You can add it to the expression like this:

$article = $article -replace "(?s).*<article.*?>\s*(.*)\s*</article>.*", "$1"

Again, PowerShell accepts it without an error. The regex now matches, but I end up with an empty string! That is the second, independent failure.

Quotation matters!

The second problem has to do with quotation. To me as a .NET developer, a double quote starts a string ("hello") and a single quote represents a char ('c'). In PowerShell, however, a double-quoted string supports interpolation: "hello $name". PowerShell expands $1 before the replacement string reaches the regex engine. With no $1 variable, that produces an empty replacement; if a $1 variable exists, its value is substituted instead.

The following is more PowerShell-esque and actually works:

$article = $article -replace '(?s).*<article.*?>\s*(.*)\s*</article>.*', '$1'

But I love my double quotes...

If you are adamant about using double quotes, use PowerShell's backtick escape character (`) before the dollar sign so PowerShell passes it through literally:

$article = $article -replace "(?s).*<article.*?>\s*(.*)\s*</article>.*", "`$1"

What about named capture replacement?

Sometimes named capture groups improve the readability of your code. Because they also use a dollar sign, they suffer from the same problem. Use a single-quoted replacement string when interpolation is not required:

$article = $article -replace '(?s).*<article.*?>\s*(?<content>.*)\s*</article>.*', '${content}'

Did you know that ${1} also works?

Final thoughts

PowerShell runs on .NET and uses the .NET regex engine, but PowerShell string interpolation happens before the regex engine sees the replacement. Single quotes prevent that interpolation, but they do not guarantee a correct regex: your pattern and options still need to match the input.

Funnily enough it was not the first time I had problems with regular expressions that looked similar to .NET; read this article about regular expression groups in JavaScript.

Improvements

2020-06-06: rewrote the article to reflect the problems with quotation and new-line matching.

expand_less brightness_auto