jennab
jennab

Reputation: 305

Renaming file names and content within files respecting the original case using Powershell

Question regarding renaming of files and content within files using Powershell following the solution here.

With the script below, all file names and occurrences within the files are renamed. The replacement is case-insensitive, i.e. no matter if there is an occurrence "uvw", "UVW", "Uvw", etc., the replacement is "XYZ". Is it possible to respect the case of the original and rename "true to original", i.e. "uvw" -> "xyz", "UVW" -> "XYZ", "Uvw" -> "Xyz" (also "abc_123" should be "def_123" and not "DEF_123" by default)?

$filePath = "C:\root_folder"
$include = '*.txt', '*.xml' # adapt as needed
Get-ChildItem -File $filePath -Recurse -Include $include | 
 Rename-Item -WhatIf -PassThru -NewName { $_.Name -replace 'UVW', 'XYZ' } |
 ForEach-Object {
 ($_ | Get-Content -Raw) -replace 'ABC_123', 'DEF_123' |
   Set-Content -NoNewLine -LiteralPath $_.FullName
}

Upvotes: 2

Views: 55

Answers (2)

Santiago Squarzon
Santiago Squarzon

Reputation: 61103

So, this is incredibly inefficient but I don't see a way around it, you need to use a match evaluator and a hashtable to map the matched characters with their replacement character.

The code will look different depending on if you're on PowerShell 7+ where Replacement with a script block exists or if you're on Windows PowerShell 5.1 where you need to call the Regex.Replace API targeting one of the MatchEvaluator overloads).

  • PowerShell 7+
$evaluator = {
    # enumerate each character from the matched value
    # `.Value` in this context from the examples would be `uvw`, `UVW` and `Uvw`
    $result = foreach ($char in $_.Value.GetEnumerator()) {
        # here we get the replacement character
        # i.e.:
        #  - if `$char` is `u` then `$value` is `x`
        #  - if `$char` is `v` then `$value` is `y`
        $value = $map[$char.ToString()]
        # check if the enumerated character is uppercase
        if ([char]::IsUpper($char)) {
            # then, we need to output the uppercase replacement char too
            $value.ToUpper()
            # and go to the next char
            continue
        }
        
        # else, just output the char as-is (lowercase)
        $value
    }

    # lastly, after all matched characters are processed,
    # create a new string from the char array
    [string]::new($result)
}

$map = @{
    u = 'x'
    v = 'y'
    w = 'z'
}

'foo uvw bar' -replace 'uvw', $evaluator # Outputs: foo xyz bar
'foo UVW bar' -replace 'uvw', $evaluator # Outputs: foo XYZ bar
'foo Uvw bar' -replace 'uvw', $evaluator # Outputs: foo Xyz bar
  • Windows PowerShell 5.1:
$evaluator = {
    $result = foreach ($char in $args[0].Value.GetEnumerator()) {
        $value = $map[$char.ToString()]
        if ([char]::IsUpper($char)) {
            $value.ToUpper()
            continue
        }

        $value
    }

    [string]::new($result)
}

$map = @{
    u = 'x'
    v = 'y'
    w = 'z'
}

$re = [regex]::new('uvw', [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$re.Replace('foo uvw bar', $evaluator) # Outputs: foo xyz bar
$re.Replace('foo UVW bar', $evaluator) # Outputs: foo XYZ bar
$re.Replace('foo Uvw bar', $evaluator) # Outputs: foo Xyz bar

Upvotes: 2

mklement0
mklement0

Reputation: 440227

An alternative approach to Santiago's helpful answer, which too relies on a match-evaluator script block that is called for every match:

The general idea is:

  • Loop over as many characters that the matched text and the replacement text share
  • Use a System.Text.StringBuilder instance to build a case-matched version of the replacement text, character by character.
  • Any additional characters in the replacement string (those that have no counterpart in the matched text) are retained as-is.

Both solutions below output <XyZ>, as intended.

PowerShell (Core) 7+ solution:

# Sample input string
$inputString = '<UvW>'
# Sample search pattern.
$searchPattern = 'uvw'
# Sample replacement text.
$replaceWith = 'xyz'

# A string builder to make constructing the case-matched replacement string
# more efficient.
$caseMatchedReplaceWith = [System.Text.StringBuilder]::new($replaceWith.Length)

$inputString -replace $searchPattern, {  
  $matchedText = $_.Value
  $numCharsToMatch = [Math]::Min($replaceWith.Length, $matchedText.Length)
  $null = $caseMatchedReplaceWith.Clear()
  foreach ($i in 0..($numCharsToMatch-1)) {
    $replacementChar = $replaceWith[$i]
    if ([char]::IsUpper($matchedText[$i]) -and -not [char]::IsUpper($replacementChar)) {
      $replacementChar = [char]::ToUpper($replacementChar)
    } 
    $null = $caseMatchedReplaceWith.Append($replacementChar)
  }
  $caseMatchedReplaceWith.ToString() + $replaceWith.Substring($numCharsToMatch)
}

Windows PowerShell solution (where using a script block as the substitution operand of -replace isn't supported):

# Sample input string
$inputString = '<UvW>'
# Sample search pattern.
$searchPattern = 'uvw'
# Sample replacement text.
$replaceWith = 'xyz'

# A string builder to make constructing the case-matched replacement string
# more efficient.
$caseMatchedReplaceWith = [System.Text.StringBuilder]::new($replaceWith.Length)

[regex]::Replace(
  $inputString,
  $searchPattern, 
  {  
    $matchedText = $args[0].Value
    $numCharsToMatch = [Math]::Min($replaceWith.Length, $matchedText.Length)
    $null = $caseMatchedReplaceWith.Clear()
    foreach ($i in 0..($numCharsToMatch-1)) {
      $replacementChar = $replaceWith[$i]
      if ([char]::IsUpper($matchedText[$i]) -and -not [char]::IsUpper($replacementChar)) {
        $replacementChar = [char]::ToUpper($replacementChar)
      } 
      $null = $caseMatchedReplaceWith.Append($replacementChar)
    }
    $caseMatchedReplaceWith.ToString() + $replaceWith.Substring($numCharsToMatch)
  },
  'IgnoreCase'
)

Upvotes: 2

Related Questions