Issue with Powershell update string in each file within a folder

vonPryz and marsze have provided the crucial pointers: since the -replace operator operates on regexes (regular expressions), you must \-escape special characters such as [ and ] in order to treat them verbatim (as literals):
$_ -replace '\[dbo_202001\]', '[dbo_201902]'

While use of the -replace operator is generally preferable, the [string] type’s .Replace() method directly offers verbatim (literal) string replacements and is therefore also faster than -replace.
Typically, this won’t matter, but in situations similar to yours, where many iterations are involved, it may (note that the replacement is case-sensitive):
$_.Replace('[dbo_202001]', '[dbo_201902]')

See the bottom section for guidance on when to use -replace vs. .Replace().


The performance of your code can be greatly improved:

$sourceDir="C:\SQL_Scripts"
foreach ($file in Get-ChildItem -File $sourceDir -Filter *.sql -Recurse)
{
  # CAVEAT: This overwrites the files in-place.
  Set-Content -NoNewLine $file.PSPath -Value `
    (Get-Content -Raw $file.PSPath).Replace('[dbo_202001]', '[dbo_201902]')
}
  • Since you’re reading the whole file into memory anyway, using Get-Content‘s -Raw switch to read it as a single, multi-line string (rather than an array of lines) on which you can perform a single .Replace() operation is much faster.

  • Set-Content‘s -NoNewLine switch is needed to prevent an additional newline from getting appended on writing back to the file.

  • Note the use of the -Value parameter rather than the pipeline to provide the file content. Since there’s only a single string object to write here, it makes little difference, but in general, with many objects to write that are already collected in memory, Set-Content ... -Value $array is much faster than $array | Set-Content ....


Guidance on use of the -replace operator vs. the .Replace() method:

Note that both features invariably replace all matches they find, and, conversely, return the original string if none are found.

Generally, PowerShell’s -replace operator is a more natural fit in PowerShell code – both syntactically and due to its case-insensitivity – and offers more functionality, thanks to being regex-based.

The .Replace() method is limited to verbatim replacements and in Windows PowerShell to case-sensitive ones, but has the advantage of being faster and not having to worry about escaping special characters in its arguments:

  • Only use the [string] type’s .Replace() method:

    • for invariably verbatim string replacements
    • with the following case-sensitivity:
      • PowerShell [Core] v6+: case-sensitive by default, optionally case-insensitive via an additional argument; e.g.:

        'FOO'.Replace('o', '@', 'InvariantCultureIgnoreCase')
        
      • Windows PowerShell: invariably(!) case-sensitive

    • if functionally feasible, when performance matters
  • Otherwise, use PowerShell’s -replace operator (covered in more detail here):

    • for regex-based replacements:

      • enables sophisticated, pattern-based matching and dynamic construction of replacement strings

      • To escape metacharacters (characters with special syntactic meaning) in order to treat them verbatim:

        • in the pattern (regex) argument: \-escape them (e.g., \. or \[)
        • in the replacement argument: only $ is special, escape it as $$.
      • To escape an entire operand in order to treat its value verbatim (to effectively perform literal replacement):

        • in the pattern argument: call [regex]::Escape($pattern)

        • in the replacement argument: call $replacement.Replace('$', '$$')

    • with the following case-sensitivity:

      • case-insensitive by default
      • optionally case-sensitive via its c-prefixed variant, -creplace
    • Note: -replace is a PowerShell-friendly wrapper around the [regex]::Replace() method that doesn’t expose all of the latter’s functionality, notably not its ability to limit the number of replacements; see this answer for how to use it.

Note that -replace can directly operation on arrays (collections) of strings as the LHS, in which case the replacement is performed on each element, which is stringified on demand.

Thanks to PowerShell’s fundamental member-access enumeration feature, .Replace() too can operate on arrays, but only if all elements are already strings. Also, unlike -replace, which always also returns an array if the LHS is one, member-access enumeration returns a single string if the input object happens to be a single-element array.

As an aside: similar considerations apply to the use of PowerShell’s -split operator vs. the [string] type’s .Split() method – see this answer.

Examples:

-replace – see this answer for syntax details:

# Case-insensitive replacement.
# Pattern operand (regex) happens to be a verbatim string.
PS> 'foo' -replace 'O', '@'
f@@

# Case-sensitive replacement, with -creplace
PS> 'fOo' -creplace 'O', '@'
f@o

# Regex-based replacement with verbatim replacement: 
# metacharacter '$' constrains the matching to the *end*
PS> 'foo' -replace 'o$', '@'
fo@

# Regex-based replacement with dynamic replacement: 
# '$&' refers to what the regex matched
PS> 'foo' -replace 'o$', '>>$&<<'
fo>>o<<

# PowerShell [Core] only:
# Dynamic replacement based on a script block.
PS> 'A1' -replace '\d', { [int] $_.Value + 1 }
A2

# Array operation, with elements stringified on demand:
PS> 1..3 -replace '^', '0'
01
02
03

# Escape a regex metachar. to be treated verbatim.
PS> 'You owe me $20' -replace '\$20', '20 dollars'
You owe me 20 dollars. 

# Ditto, via a variable and [regex]::Escape()
PS> $var="$20"; 'You owe me $20' -replace [regex]::Escape($var), '20 dollars'
You owe me 20 dollars.

# Escape a '$' in the replacement operand so that it is always treated verbatim:
PS> 'You owe me 20 dollars' -replace '20 dollars', '$$20'
You owe me $20

# Ditto, via a variable and [regex]::Escape()
PS> $var="$20"; 'You owe me 20 dollars' -replace '20 dollars', $var.Replace('$', '$$')
You owe me $20.

.Replace():

# Verbatim, case-sensitive replacement.
PS> 'foo'.Replace('o', '@')
f@@

# No effect, because matching is case-sensitive.
PS> 'foo'.Replace('O', '@')
foo

# PowerShell [Core] v6+ only: opt-in to case-INsensitivity:
PS> 'FOO'.Replace('o', '@', 'InvariantCultureIgnoreCase')
F@@

# Operation on an array, thanks to member-access enumeration:
# Returns a 2 -element array in this case.
PS> ('foo', 'goo').Replace('o', '@')
f@@
g@@

# !! Fails, because not all array elements are *strings*:
PS> ('foo', 42).Replace('o', '@')
... Method invocation failed because [System.Int32] does not contain a method named 'Replace'. ...

Leave a Comment