Home > Software engineering >  PowerShell: Replace string in all .txt files within directory
PowerShell: Replace string in all .txt files within directory

Time:07-26

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.

CodePudding user response:

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 2 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.

Thank you for all your help.

CodePudding user response:

  • 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 .

  • Related