Search code examples
powershellreplaceescaping

Preventing "$_" from being omitted from variable content


I am reading a large blob of text that I regex out of another like so

$myVar = Select-String -Pattern '(?<=search\s=\s).*[\s*](?s).*' -input $myContent -AllMatches | Foreach {$_.matches}

The regex works well. The content of $myVar is code (can be powershell too), so it can contain special characters. As a result of this, $myVar sometimes contains the character "$_", which is also the $PSItem in Powershell. When I try to process $myVar, I get issues because Powershell is treating the $_ as code, and not a string. I'm looking for a way to escape this character.

I've tried using Regex::Escape() but this literally replaces every special character with their "escaped version". To the point where my string loses integrity and none of my existing processing works on it (because it's content has changed). I'm looking for a way that will keep my string intact without having PS interpret the special character. So far, I've had no problems with other special characters ($, ^, *, +, ?). It's just this one edge case of $_ that breaks my processing.

Here's an snippet of what $myVar could contain

bucket = testdata \
| search NOT CommandLine IN ("\"C:\\Windows\\system32\\cmd.exe\" /c PowerShell.exe -command \"get-Hotfix *", "\"C:\\Windows\\system32\\cmd.exe\" /c PowerShell.exe -command \"(Get-ItemProperty -Path HKLM:\\SOFTWARE\\Microsoft\\PowerShell\\3\\PowerShellEngine -Name 'PowerShellVersion').PowerShellVersion\"", "\"C:\\Windows\\system32\\cmd.exe\" /c (wmic service where 'name like \"%%*%%\"' get name, startname /format:csv)", "\"C:\\Windows\\system32\\cmd.exe\" /c powershell -c \"(Get-Counter '\\Process(*)\\%% Processor Time').Countersamples | Where-Object cookedvalue -gt 0 | Sort-Object cookedvalue -Desc | Select-Object -First 10 | Format-Table -a instancename, @{Name='CPU %%';Expr={[Math]::Round($_.CookedValue)}}","\"C:\\Windows\\system32\\cmd.exe\" /c powershell.exe -c \"Get-CIMInstance -Class Win32_logicaldisk | Select-Object Caption, Freespace\"")\

The above is just a snippet, the actual variable can contain 100s of such lines.

In the above however, you can see the line ;Expr={[Math]::Round($_.CookedValue)}}. The $_ is where I'm having trouble.

Can someone help me find a way to not have PS interpret the content of my string literally, without morphing the actual content of the variable?


Solution

  • Preface:

    • In PowerShell, $ chars. only have special meaning in expandable ("...") string literals: they are interpreted as the start of a variable name (e.g. $var) or subexpression (e.g., $(1+2)) to interpolate, i.e. to replace with their value.

    • In (possibly resulting-from-literals) string values stored in variables or properties, they do not (except if passed to Invoke-Expression, which is best avoided).


    In other words: there isn't a problem with $_ being stored in the value of variable $myVar per se - PowerShell won't interpret that value unless explicitly asked to do so.

    The problem turned out to be the use of $myVar in a -replace operation:

    $myContent -replace '(?<=search\s=\s).*[\s*](?s).*', $myVar

    As in the analogous parameter of the underlying [regex]::Replace() .NET API, in the substitution operand in a -replace operation $ also happens to have special meaning, but that meaning is specific to the .NET regex engine and unrelated to PowerShell:

    • In the substitution operand, $-prefixed tokens refer to parts of what the search operand matched; the search operand is the first RHS operand, and it is invariably interpreted as a regex, such as 'f(o+)' in the following example:

       # $1 refers to what the 1st (...) (capture group) matched.
       'foo' -replace 'f(o+)', 'g$1'  # -> 'goo'
      
    • To use verbatim $ chars. in the substitution operand, escape them by doubling them ($$)

       'foo' -replace 'f(o+)', 'g$$'  # -> 'g$'
      

    To escape all $ chars. in a substitution operand programmatically, it is simplest to use the .Replace() string method; therefore:

    $myContent -replace '(?<=search\s=\s).*[\s*](?s).*', $myVar.Replace('$', '$$')
    

    ($myVar -replace '\$', '$$$$' would work too, but may be confusing.)

    Note:

    • In the substitution operand - which are themselves not regexes - $ is the only metacharacter that requires escaping.

    • In the search operand, because it is a regex, many more metacharacters are recognized; while they can be \-escaped individually for verbatim use, the robust way to escape them programmatically is via [regex]::Escape().

    For a concise but comprehensive overview of the -replace operator, see this answer.