powershell multiple block expressions - powershell

I am replacing multiple strings in a file. The following works, but is it the best way to do it? I'm not sure if doing multiple block expressions is a good way.
(Get-Content $tmpFile1) |
ForEach-Object {$_ -replace 'replaceMe1.*', 'replacedString1'} |
% {$_ -replace 'replaceMe2.*', 'replacedString2'} |
% {$_ -replace 'replaceMe3.*', 'replacedString3'} |
Out-File $tmpFile2

You don't really need to foreach through each replace operations. Those operators can be chained in a single command:
#(Get-Content $tmpFile1) -replace 'replaceMe1.*', 'replacedString1' -replace 'replaceMe2.*', 'replacedString2' -replace 'replaceMe3.*', 'replacedString3' |
Out-File $tmpFile2

I'm going to assume that your patterns and replacements don't really just have a digit on the end that is different, so you might want to execute different code based on which regex actually matched.
If so you can consider using a single regular expression but using a function instead of a replacement string. The only catch is you have to use the regex Replace method instead of the operator.
PS C:\temp> set-content -value #"
replaceMe1 something
replaceMe2 something else
replaceMe3 and another
"# -path t.txt
PS C:\temp> Get-Content t.txt |
ForEach-Object { ([regex]'replaceMe([1-3])(.*)').Replace($_,
{ Param($m)
$head = switch($m.Groups[1]) { 1 {"First"}; 2 {"Second"}; 3 {"Third"} }
$tail = $m.Groups[2]
"Head: $head, Tail: $tail"
})}
Head: First, Tail: something
Head: Second, Tail: something else
Head: Third, Tail: and another
This may be overly complex for what you need today, but it is worth remembering you have the option to use a function.

The -replace operator uses regular expressions, so you can merge your three script blocks into one like this:
Get-Content $tmpFile1 `
| ForEach-Object { $_ -replace 'replaceMe([1-3]).*', 'replacedString$1' } `
| Out-File $tmpFile2
That will search for the literal text 'replaceMe' followed by a '1', '2', or '3' and replace it with 'replacedString' followed by whichever digit was found (the '$1').
Also, note that -replace works like -match, not -like; that is, it works with regular expressions, not wildcards. When you use 'replaceMe1.*' it doesn't mean "the text 'replaceMe1.' followed by zero or more characters" but rather "the text 'replaceMe1' followed by zero or more occurrences ('*') of any character ('.')". The following demonstrates text that will be replaced even though it wouldn't match with wildcards:
PS> 'replaceMe1_some_extra_text_with_no_period' -replace 'replaceMe1.*', 'replacedString1'
replacedString1
The wildcard pattern 'replaceMe1.*' would be written in regular expressions as 'replaceMe1\..*', which you'll see produces the expected result (no replacement performed):
PS> 'replaceMe1_some_extra_text_with_no_period' -replace 'replaceMe1\..*', 'replacedString1'
replaceMe1_some_extra_text_with_no_period
You can read more about regular expressions in the .NET Framework here.

Related

PowerShell: Replace string in all .txt files within directory

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.
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:
Replace was not working as I intended, so I had to use .replace instead
The original Get-ChildItem was not returning any values and had to be replaced with the above version.
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+.

how to replace text ($) with quotation marks in a text file by using powershell

I am trying to replace the text ($) with quotation marks in a text file by using PowerShell
Here is the code I am trying, But not giving expected results:
$FilePath = "C:\PT\Test\"
Get-ChildItem $FilePath -Filter *.TXT | ForEach-Object {
(Get-Content $_.FullName) | Foreach-Object {
$_ -replace ',"$"', ',"$",'
} | Set-Content $_.FullName
}
As pointed out by Abraham in his comment, the $ is a reserved character in regex known as Anchor, if you want to match a literal $ using -replace you would need to escape it: \$:
Get-ChildItem $FilePath -Filter *.TXT | ForEach-Object {
(Get-Content $_.FullName -Raw) -replace ',"\$"',',"$",' |
Set-Content $_.FullName
}
The other alternative is to use the .Replace(..) string method which will match literal characters.
Get-ChildItem $FilePath -Filter *.TXT | ForEach-Object {
(Get-Content $_.FullName -Raw).Replace(',"$"',',"$",') |
Set-Content $_.FullName
}
By looking at your code seems like you want to replace ,"$" with ,"$",, if this was not the case let me know.
The -replace operator:
uses a regex as the search-pattern operand, and therefore requires a verbatim $ to be escaped as \$ in order to be interpreted as such (an unescaped $ is a regex metacharacter representing the end of the input string)
also uses $ as a metacharacter in the substitution operand, namely to refer to the text that the regex captured ($&) or parts thereof (e.g. $1 to refer to what the first capture group ((...)) captured). In this case, escape it as $$.
Note: Situationally, even an unescaped $ may work, namely if, in combination with the subsequent characters, if any, it cannot be interpreted as a placeholder such as $& or $1. However, in the interest of robustness it is better to escape.
Therefore, use:
PS> ',"$"20' -replace ',"\$"', ',"$$",'
,"$",20
Taking a step back:
As Abraham Zinala suggests, for verbatim string replacements, the .Replace() string method is sufficient, whose use doesn't necessitate escaping, and which is faster to boot:
PS> ',"$"20'.Replace(',"$"', ',"$",')
,"$",20
Caveat: Unlike -replace, .Replace() is case-sensitive - invariably in Windows PowerShell and by default in PowerShell (Core) 7+.
See this answer for a detailed juxtaposition of -replace and .Replace(), including how to perform escaping for -replace programmatically.
To put it all together, along with an optimization that reads each file in full, with Get-Content -Raw, for much faster processing (to avoid appending a trailing newline, -NoNewLine is used with Set-Content):
$FilePath = "C:\PT\Test"
Get-ChildItem $FilePath -Filter *.TXT | ForEach-Object {
(Get-Content -Raw $_.FullName).Replace(',"$"', ',"$",') |
Set-Content -NoNewLine $_.FullName
}

Powershell: how to easily switch between 'Out-String' and 'Get-Content' string variable types?

I want to use the method given in the answer of this question:
PowerShell - Remove all lines of text file until a certain string is found
However I don't get my string from "Get-Content"; I get it from "Out-String". How can I convert my "Out-String" variable into a "Get-Content" format without needing to "Set-Content"/"Get-Content" a temporary file? Or how can I get the same end result without even converting?
It really hurts my brains that a "Get-Member" on the variable from either 'Out-String' or 'Get-Content' returns a TypeName of System.String but you cannot use them the same way...
Here is the simplified code I've been trying to understand - let's use that:
# Let's work with the text from 'Get-Help' output:
$myString = (Get-Help | out-string)
# I only want the text from the "SEE ALSO" section:
$cut = $myString.Where({ $_ -like ("*SEE ALSO*") },'SkipUntil')
$cut # This shows the whole thing!!! :-(
$cut | gm | findstr TypeName # says 'TypeName: System.String'
# Dirty conversion to "Get-Content" format:
Set-Content "tmp.file" -value $cut
$recut = (Get-Content "tmp.file").Where({ $_ -like ("*SEE ALSO*") },'SkipUntil')
$recut # Now this shows what I want, even though the below returns 'TypeName: System.String' as well !!!
(Get-Content "tmp.file") | gm | findstr TypeName
The problem is get-help (with no parameters) or out-string is outputting one multiline string (with windows line endings). I even tried out-string -stream. This is unusual for a powershell command. Get-content would split up the lines for you automatically.
(get-help).count
1
One way to resolve it is to split on the line endings. I'm also skipping blank lines at the end. (This split pattern works with unix/osx text too.)
((get-help) -split '\r?\n').Where({ $_ -like '*SEE ALSO*' },'SkipUntil') | where { $_ }
SEE ALSO:
about_Updatable_Help
Get-Help
Save-Help
Update-Help
Or:
((get-help) -split '\r?\n').Where({ $_ -match 'SEE ALSO' },'SkipUntil').Where{ $_ }
In this case, you do not even need Out-String, but I will stick to your example:
$myString = (Get-Help | Out-String)
$mystring -match "(?ms)^.*(SEE\sALSO.*)$" | Out-Null
$Matches[1]
The key in the regex is (?ms). m enables multi-line search and s enables wildcards to span over multiple lines (in other words: including line breaks). The result of the -match operator is piped to Out-Null to not see it in the terminal. You might want to evaluate it though. If $true, $Matches[1] will contain your desired string.

powershell filter filenames with regex

I am building a list of files that I'm putting into my $list variable.
Then I want to filter the list based on the $filter variable. The current solution works, but it doesn't work with a regex.
$filter = #("test.txt","Fake","AnotherFile\d{1..6}")
######### HTML TESTS #############
[string]$list = #"
FakeFile.txt
test120119.txt
AnotherFile120119.txt
LastFile.txt
"#
[array]$files = $list -split '\r?\n'
$files = $files | Where-Object {$_} | Where {$_ -notin $filter} # filter out empty items from the array...
$files
My idea is to put regex patterns in the $filter variable so I can catch filenames that have datestamps in them such as test120119.txt in the $list variable above.
How can I change my code to allow for regex? I tried some variations of select-string without splitting my $list, but was not fruitful. I also tried changing my -notin to -notmatch but this doesn't work at all of course.
If you want to use regex, I think it would be easier to just fully commit to regex with your $filter array.
$filter = "^test\d{0,6}\.txt","^Fake","^AnotherFile\d{0,6}\.txt" -join '|'
$list = #"
FakeFile.txt
test120119.txt
AnotherFile120119.txt
LastFile.txt
"#
$files = $list -split '\r?\n'
$files | Where {$_ -notmatch $filter}
The thing to keep in mind is remembering to escape special regex characters if you want them treated literally. You can use the [regex]::Escape() method to do this for you but not if you already purposely injected regex characters.
Once you have your regex filter list, you can join each item with a regex or using the | character.
Not all operators recognize regex language. -match and -notmatch are among the few that do. -match and -notmatch are not case-sensitive. If you want to match against case, you should use the -c variants of the operators, namely -cmatch and -cnotmatch.
The regex items can be tweaked to your liking. More requirements would need to be given in order to come up with an exact solution. Here are some examples to consider:
\d{0,6} matches 0 to 6 consecutive digits. 122619 will match successfully, but so will 1226. If you want only 0 or 6 digits to match, you can use (\d{6})?.
^ should be used if you want to start each match at the beginning of the input string. So if you want the regex or to apply from the beginning of the string, you need to include ^ in each item or group items succeeding the initial ^ with () accordingly. ^item1|^item2 will return the same capture group 0 match as ^(item1|item2).
\ escape the literal . characters.
Not using anchor characters like ^ and $ create a lot of flexibility and potentially unwanted results. 'FakeFile' -match 'Fake' returns true but so does 'MyFakeFile' -match 'Fake'. However, 'MyFakeFile' -match 'Fake$' returns false and 'MyFake' -match 'Fake$' returns true.

Replacing String without replacing whole content of file powershell

Trying to edit this line of a file ("VoIP.Enabled "1"). I wanna change the 1 to a zero. When I change it with
$dewprefs = Get-Content .\dewrito_prefs.cfg
$dewprefs | Select-String "VoIP.Enabled" | ForEach-Object {$_ -replace "1","0"} | Set-Content .\dewrito_prefs.cfg}`
However when I use this script, it removes 100 other lines, edits the right line, then deletes everything else, just leaving the line I wanted to edit.
Any help on this matter would be highly appreciated
Select-String acts as a filter: that is, the input it is given is only passed out if it matches a pattern.
Therefore, only the line of interest is written to the output file.
Do not use Select-String if all input lines - though possibly modified - should be passed through; use only ForEach-Object, and conditionally modify each input line:
$dewprefs = Get-Content .\dewrito_prefs.cfg
$dewprefs |
ForEach-Object { if ($_ -match 'VoIP\.Enabled') { $_ -replace '1', '0' } else { $_ } } |
Set-Content .\dewrito_prefs.cfg
$_ -match 'VoIP\.Enabled' now does what Select-String did in your original command: it matches only if the input line at hand contains literal VoIP.Enabled (note how the . is escaped as \. to ensure that is treated as a literal in the context of a regular expression).
Note how both branches of the if statement produce output:
$_ -replace '1', '0' outputs the result of replacing all instances of 1 in the input line with 0
$_ simply passes the input line through as-is.
Most likely you could replace the if statement with a single -replace expression, however, and, assuming that the file is small enough to be read as a whole (quite likely, in the case of a configuration file), you can use a variant of Stu's helpful simplification.
Taking full advantage of the fact that -replace supports regexes (regular expressions), the code can update lines based on a key name such as VoIP.Enabled only, without needing to know that key's current value.
$key = 'VoIP.Enabled'
$newValue = '1'
# Construct a regex that matches the entire target line.
$regex = '^\s*' + [regex]::Escape($key) + '\b.*$'
# Build the replacement line.
$modifiedLine = "$key $newValue"
(Get-Content .\dewrito_prefs.cfg) -replace $regex, $modifiedLine | Set-Content .\dewrito_prefs.cfg
Note that writing the output back to the input file only works because the input file was read into memory as a whole, up front, due to enclosing the Get-Content call in (...).
This will work too, with PowerShell v3+, and is a little more succinct:
(Get-Content .\dewrito_prefs.cfg).replace('"VoIP.Enabled "1"', '"VoIP.Enabled "0"') |
Set-Content .\dewrito_prefs.cfg
Your quotes are a little strange (3 double quotes in total?), I've mimicked what you've asked, however.