Powershell: Dollar Sign Gets 'Eaten' by -replace When More Than One Dollar Sign Adjacent In String

Created on 8 May 2018  路  6Comments  路  Source: PowerShell/PowerShell

If you try to do a -replace with a string that has two or more dollar signs next to it, one dollar sign gets removed. This happens even if the characters are escaped.

Steps to reproduce

Scenario 1: Two Dollar Signs in String Literal
image

Scenario 2: More Than Two Dollar Signs in String Literal
image

Scenario 3: One Dollar Sign in String Literal
image

Scenario 4: Two Dollar Signs in String Literal (escaped)
image

Expected behavior

Scenario 1: P@$$w0rd
Scenario 2: P@$$$w0rd
Scenario 3: P@$w0rd
Scenario 3: P@$$w0rd

Actual behavior

Scenario 1:
image

Scenario 2:
image

Scenario 3:
image

Scenario 4:
image

Environment data

$PSVersionTable
image

```

Issue-Question Resolution-Answered

Most helpful comment

The -replace replacement operand is not a _literal_: $ has special meaning in it and is used to refer to aspects of the regex matching, such as $& to refer to the entire match and $1 to refer to what the 1st capture group matched.

Therefore, $ characters you want to treat _literally_ must be _escaped_ by _doubling_ them.
(Note that this escaping mechanism is unrelated to PowerShell's `-based escaping).

And that is what happened accidentally in your case: the $$ sequence was interpreted as an _escaped_ $.

Unescaped $ sequences that happen not to form a valid regex-match reference are also retained as literals, but it's better not to rely on that.

You can read more about this here.

The robust workaround is to use the following:

$testString -replace '...', $password.Replace('$', $$')

However, if your replacement operation doesn't actually need _regex_ matching, only simple _substring_ replacement, you can use [string]::Replace() for the entire operation, but note that it is case-_sensitive_ by default:

$testString.Replace('...', $password)

All 6 comments

The -replace replacement operand is not a _literal_: $ has special meaning in it and is used to refer to aspects of the regex matching, such as $& to refer to the entire match and $1 to refer to what the 1st capture group matched.

Therefore, $ characters you want to treat _literally_ must be _escaped_ by _doubling_ them.
(Note that this escaping mechanism is unrelated to PowerShell's `-based escaping).

And that is what happened accidentally in your case: the $$ sequence was interpreted as an _escaped_ $.

Unescaped $ sequences that happen not to form a valid regex-match reference are also retained as literals, but it's better not to rely on that.

You can read more about this here.

The robust workaround is to use the following:

$testString -replace '...', $password.Replace('$', $$')

However, if your replacement operation doesn't actually need _regex_ matching, only simple _substring_ replacement, you can use [string]::Replace() for the entire operation, but note that it is case-_sensitive_ by default:

$testString.Replace('...', $password)

Thank you for explaining! I couldn't find a reference to that kind of escaping anywhere!

We should probably have include a link to Substitutions in Regular Expressions in about_Comparison_Operators as well as mentioning that substitutions happen with -replace in the body of the document.

Someone forgot to post that the symptom exist in PSCore6. Although I would have use the following .NET object replace method:

PS C:\Program Files\PowerShell\6-preview> $password
P@$$word
PS C:\Program Files\PowerShell\6-preview> $password.Length
8
PS C:\Program Files\PowerShell\6-preview> $password.ToString()
P@$$word
PS C:\Program Files\PowerShell\6-preview> "$($password.ToString())"
P@$$word
PS C:\Program Files\PowerShell\6-preview> $testString.replace('%MGR_PWD',"$($password.ToString())");
P@$$word

I mostly use .NET object methods instead of using parameters. But, that's me!

@MaximoTrinidad:

To be clear: the behavior is by design, and it is here to stay.
However, the behavior is poorly documented, hence @BrucePay's suggestion.

Using String.Replace(string oldvalue, string newvalue) may be the simplest solution in _this_ case, but it is by no means a _general_ alternative to -replace; not only do the behaviors differ, -replace offers many more features:

  • Regex support, which -replace _invariably_ applies.

    • String.Replace() _only_ supports literal substring replacement - both for matching and in the replacement string.

      • This prevents advanced matching behavior, such as anchoring matches or capturing parts of the input string and referencing these parts in the replacement-string operand.
      • On the flip side, you do not have to worry about escaping $ chars. in the replacement string, as you demonstrate.
    • You can _indirectly_ get literal substring replacements with -replace too:

      • If the _search_ operand is a string literal, \-escape all regex metacharacters that you want to be treated as literals.

        • If the search operand is a _variable_ reference, call [regex]::Escape() on it.

      • If the _replacement_ operand is a string literal, $-escape $ instances that you want to be treated as literals (i.e., _double_ $ chars. meant to be literals).

        • If the search operand is a _variable_ reference, call .Replace('$', '$$') on it (i.e., take advantage of simple substring replacement via String.Replace()) or, if you want to stick with -replace, use (... -replace '\$', '$$$$').

    • As an aside: thanks to work by @IISResetMe, -replace as of v6.1.0-preview.2 even supports passing a _script block_ as the replacement operand; e.g.,
      '1 + 1 = 2' -replace '\d+', { [int] $_.Value * 2 } yields '2 + 2 = 4'

  • Case-insensitivity.

    • String.Replace(string oldValue, string newValue), is case-_sensitive_, but you can opt into case-sensitivity via overloads String.Replace(string oldValue, string newValue, bool ignoreCase, cultureinfo culture) or String.Replace(string oldValue, string newValue, System.StringComparison comparisonType)
    • Conversely, -creplace gives you case-sensitivity (more conveniently) in PowerShell.
  • Support for array-valued LHS operands.

  • Support for implicit to-string conversion of the LHS.

P.S.: You can find an analogous juxtaposition of -split and String.Split() in this SO answer.

I've created a doc issue at https://github.com/PowerShell/PowerShell-Docs/issues/2416

@blixthecat: Now that the issue is being tracked there, can I ask you to close _this_ one?

Was this page helpful?
0 / 5 - 0 ratings