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()
.
- To perform literal replacements,
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
withGet-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 withSet-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 .