1

I am trying to replace every instance of a string within a directory. However my code is not replacing anything.

What I have so far:

Test Folder contains multiple files and folders containing content that I need to change. The folders contain .txt documents, the .txt documents contain strings like this: Content reference="../../../PartOfPath/EN/EndofPath/Caution.txt" that i need to change into this: Content reference="../../../PartOfPath/FR/EndofPath/Caution.txt"

Before this question comes up, yes it has to be done this way, as there are other similar strings that I don't want to edit. So I cannot just replace all instances of EN with FR.

$DirectoryPath = "C:\TestFolder"

$Parts =@(
@{PartOne="/PartOfPath";PartTwo="EndofPath/Caution.txt"},
@{PartOne="/OtherPartOfPath";PartTwo="EndofPath/Note.txt"},
@{PartOne="/ThirdPartOfPath";PartTwo="OtherEndofPath/Warning.txt"}) | % { New-Object object | Add-Member -NotePropertyMembers $_ -PassThru }

Get-ChildItem $DirectoryPath | ForEach {
    foreach($n in $Parts){
        [string]$PartOne = $n.PartOne
        [string]$PartTwo = $n.PartTwo
        $ReplaceThis = "$PartOne/EN/$PartTwo"
        $WithThis = "$PartOne/FR/$PartTwo"
        (Get-Content $_) | ForEach  {$_ -Replace $ReplaceThis, $WithThis} | Set-Content $_
    }
}

The code will run and overwrite files, however no edits will have been made.

While troubleshooting I came across this potential cause:

This test worked:

$FilePath = "C:\TestFolder\Test.txt"

$ReplaceThis ="/PartOfPath/EN/Notes/Note.txt"
$WithThis = "/PartOfPath/FR/Notes/Note.txt"

(Get-Content -Path $FilePath) -replace $ReplaceThis, $WithThis | Set-Content $FilePath

But this test did not

$FilePath = "C:\TestFolder\Test.txt"
foreach($n in $Parts){
    [string]$PartOne = $n.PartOne
    [string]$PartTwo = $n.PartTwo

    [string]$ReplaceThis = "$PartOne/EN/$PartTwo"
    [string]$WithThis = "$PartOne/FR/$PartTwo"

    (Get-Content -Path $FilePath) -replace $ReplaceThis, $WithThis | Set-Content $FilePath
}

If you can help me understand what is wrong here I would greatly appreciate it.

  • 1
    Since you are hiding the paths, and using regex replace, I can only guess that you have some regex-incompatible characters in the real paths. Aside from bugs like using `$FR` without defining it in the top code, which should not stop the replace but only leave the name missing afterwards, or the unnecessary detour through objects which also shouldn't stop it working, I can't guess what else without specific examples to try. – TessellatingHeckler Jul 25 '22 at 17:23
  • Maybe test with `foreach {$_.Replace($ReplaceThis, $WithThis) }` – TessellatingHeckler Jul 25 '22 at 17:24
  • @TessellatingHeckler $FR was a typo when simplifying my code. Thank you for pointing it out. I have updated it accordingly. – Don Cummins Jul 25 '22 at 17:30
  • foreach {$_.Replace($ReplaceThis, $WithThis) } Did not work either. I know it doesn't have anything to do with the real paths because when they are hardcoded the regex works just fine – Don Cummins Jul 25 '22 at 17:39
  • 1
    upon further tests, foreach {$_.Replace($ReplaceThis, $WithThis) } seems to be working. I will try to implement it in my actual project and see what happens. – Don Cummins Jul 25 '22 at 17:56

2 Answers2

2

Thanks to @TessellatingHeckler 's comments I revised my code and found this solution:

$DirectoryPath = "C:\TestFolder"

$Parts =@(
@{PartOne="/PartOfPath";PartTwo="EndofPath/Caution.txt"},
@{PartOne="/OtherPartOfPath";PartTwo="EndofPath/Note.txt"},
@{PartOne="/ThirdPartOfPath";PartTwo="OtherEndofPath/Warning.txt"}) | % { New-Object object | Add-Member -NotePropertyMembers $_ -PassThru }

Get-ChildItem $LanguageFolderPath -Filter "*.txt" -Recurse | ForEach {
    foreach($n in $Parts){
        [string]$PartOne = $n.PartOne
        [string]$PartTwo = $n.PartTwo
        $ReplaceThis = "$PartOne/EN/$PartTwo"
        $WithThis = "$PartOne/FR/$PartTwo"
        (Get-Content $_) | ForEach  {$_.Replace($ReplaceThis, $WithThis)} | Set-Content $_
    }
}

There were two problems:

  1. Replace was not working as I intended, so I had to use .replace instead

  2. The original Get-ChildItem was not returning any values and had to be replaced with the above version.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
0
  • PowerShell's -replace operator is regex-based and case-insensitive by default:

    • To perform literal replacements, \-escape metacharacters in the pattern or call [regex]::Escape().
  • By contrast, the [string] type's .Replace() method performs literal replacement and is case-sensitive, invariably in Windows PowerShell, by default in PowerShell (Core) 7+ (see this answer for more information).

Therefore:

  • As TessellatingHeckler points out, given that your search strings seem to contain no regex metacharacters (such as . or \) that would require escaping, there is no obvious reason why your original approach didn't work.

  • Given that you're looking for literal substring replacements, the [string] type's .Replace() is generally the simpler and faster option if case-SENSITIVITY is desired / acceptable (invariably so in Windows PowerShell; as noted, in PowerShell (Core) 7+, you have the option of making .Replace() case-insensitive too).

  • However, since you need to perform multiple replacements, a more concise, single-pass -replace solution is possible (though whether it actually performs better would have to be tested; if you need case-sensitivity, use -creplace in lieu of -replace):

$oldLang = 'EN'
$newLang = 'FR'

$regex = @(
  "(?<prefix>/PartOfPath/)$oldLang(?<suffix>/EndofPath/Caution.txt)",
  "(?<prefix>/OtherPartOfPath/)$oldLang(?<suffix>/EndofPath/Note.txt)",
  "(?<prefix>/ThirdPartOfPath/)$oldLang(?<suffix>/OtherEndofPath/Warning.txt)"
) -join '|'

Get-ChildItem C:\TestFolder\Test.txt -Filter *.txt -Recurse | ForEach-Object {
  ($_ |Get-Content -Raw) -replace $regex, "`${prefix}$newLang`${suffix}" |
    Set-Content -LiteralPath $_.FullName
}
  • See this regex101.com page for an explanation of the regex and the ability to experiment with it.

  • The expression used as the replacement operand, "`${prefix}$newLang`${suffix}", mixes PowerShell's up-front string interpolation ($newLang, which could also be written as ${newLang}) with placeholders referring to the named capture groups (e.g. (?<prefix>...)) in the regex, which only coincidentally use the same notation as PowerShell variables (though enclosing the name in {...} is required; also, here the $ chars. must be `-escaped to prevent PowerShell's string interpolation from interpreting them); see this answer for background information.

  • Note the use of -Raw with Get-Content, which reads a text file as a whole into memory, as a single, multi-line string. Given that you don't need line-by-line processing in this case, this greatly speeds up the processing of a given file.

  • As a general tip: you may need to use the -Encoding parameter with Set-Content to ensure the desired character encoding, given that PowerShell never preserves a file's original coding when reading it. By default, you'll get ANSI-encoded files in Windows PowerShell, and BOM-less UTF-8 files in PowerShell (Core) 7+.

mklement0
  • 382,024
  • 64
  • 607
  • 775