Renaming files by reformatting existing filenames – placeholders in replacement strings used with the -replace operator

Martin Brandl’s answer provides an elegant and effective solution, but it’s worth digging deeper:

PowerShell’s -replace operator (... -replace <search>[, <replace>]):

  • Takes a regular expression as its first operand, <search> (the search expression), and invariably matches globally, i.e., it replaces all matches.

    • 'bar' -replace '[ra]', '@' -> 'b@@'
    • If you want to replace a literal string, you must either individually \-escape any regex metacharacters (e.g. .) or use the result from a [regex]::Escape() call:
      • 'bar.none' -replace '\.', '-' -> 'bar-none'
      • 'bar.none' -replace [regex]::Escape('.'), '-' -> 'bar-none'
  • Specifying a replacement expression, <replace>, is optional, in which case the empty string is substituted for what <search> matched, resulting in its effective removal.

    • 'bar' -replace '[ra]' -> 'b'
  • If <replace> is specified, it supports two forms:

    • v6.1+ (PowerShell Core only): A script block ({ ... }) as the replacement operand, which offers fully dynamic calculation of the replacement string on a per-match basis:

      • 'Level 41' -replace '\d+', { 1 + $_.Value } -> 'Level 42'
    • A string containing an expression that can reference what the regular expression captured (and didn’t capture) – such as $& to refer to what was matched – explained in detail below.

      • 'bar' -replace '[ra]', '{$&}' -> 'b{a}{r}'
  • -replace matches case-insensitively (and can also be written as -ireplace); to perform case-sensitive matching, use the form -creplace.

The “replacement language” for referencing regex captures in a string-typed <replace> operand is itself not a regular expression – no matching happens there, only references to the results of the regex matching are supported, via $-prefixed placeholders that are not to be confused with PowerShell variables.

  • PowerShell’s documentation (now) briefly explains the syntax of replacement strings in its conceptual about_Comparison_Operators help topic.

  • For the full picture, refer to the Substitutions in Regular Expressions .NET framework help topic, which is applicable because PowerShell’s -replace uses the Regex.Replace() method
    behind the scenes.

For convenience, here are the references supported in the <replace> string (excerpted from the page linked above, with emphasis and annotations added):

  • $number (e.g., $1) … Includes the last substring matched by the capture group that is identified by the 1-based number in the replacement string:

    • Including (...), a parenthesized subexpression, in the regex implicitly creates a capture group (capturing group). By default, such capture groups are unnamed and must be referenced by their 1-based (decimal) index reflecting the order in which they appear in the regex, so that $1 refers to what the 1st group in your regex captured, $2 to what the 2nd captured, …

    • The form ${number} (e.g., ${1}) for disambiguation of the number is also supported (e.g., to make sure that $1 is recognized even if followed by, say, 000, use ${1}000).

    • Instead of relying on indices to refer to unnamed capture groups, you can name capture groups and refer to them by name – see next point.

    • If you’re not interested in what the capture group matched, you can opt to ignore it by turning it into a non-capturing group with (?:...).

  • ${name} … Includes the last substring matched by the named capture group that is designated by (?<name>...) in the replacement string.

  • $$ … Includes a single "$" literal in the replacement string.

  • $& … Includes a copy of the entire match in the replacement string ($0 works too, even though it isn’t directly documented).

  • $` … Includes the text of the input string before the match in the replacement string.

  • $' … Includes the text of the input string after the match in the replacement string.

  • $+ … Includes the last group captured in the replacement string. [This relieves you of the need to know the last group’s specific index.]

  • $_ … Includes the entire input string in the replacement string.

Finally, note that:

  • -replace invariably matches globally, so if the input string contains multiple matches, the replacements above apply to each match.

  • It is generally preferable to use '...' (single quotes) for both the regex and the replacement string, because single-quoted strings are non-expanding (non-interpolating), and therefore avoid confusion with PowerShell’s own up-front expansions of $-prefixed tokens and interpretation of ` chars.
    If you do need to include PowerShell variables or expressions, you have three options:

    • Use "..." (expandable strings) and `-escape $ instances that are meant for the regex engine; e.g., `$1 in the following example:
      'abc' -replace '(a)', "[`$1]-$HOME-"
      which yields something like [a]-C:\Users\jdoe-bc

    • Build your string from literal pieces and variable references using string concatenation (+); e.g.:
      'abc' -replace '(a)', ('[$1]-' + $HOME + '-')

    • Use -f, the string-formatting operator string concatenation; e.g.:
      'abc' -replace '(a)', ('[$1]-{0}-' -f $HOME)

  • Given that you need to use $$ to escape a literal $ in the replacement string, use the following idiom to use a variable whose value you want to use literally:

    • ... -replace <search>, $var.Replace('$', '$$')
    • This relies on the [string]::Replace() method performing literal substring replacements.
      On a side note, this method is an alternative to -replace in simple cases, but note that it is case-sensitive by default.
    • Alternatively, use a nested -replace operation, but the syntax is tricky due to the escaping requirements:
      ... -replace <search>, ($var -replace '\$', '$$$$')

Leave a Comment