How to - Find and replace the first occurrence only - powershell

I have a script that seems to work correctly only it works to good.
I have files that contain multiple lines with the string "PROCEDURE DIVISION.", with the period at the end.
What I need to do...
ONLY remove the [2nd occurrence] of the string "PROCEDURE DIVISION." if it's in the text file twice and bypass the file if it is only found once. I need to preserve the 1st occurrence and change/remove the 2nd occurrence.
I can find and replace all the occurrences easily, I have no clue how to replace only 1 of 2.
Is this possible using Powershell?
Here is my code so far...
Get-ChildItem 'C:\Temp\*.cbl' -Recurse | ForEach {#
(Get-Content $_ | ForEach { $_ -replace "PROCEDURE DIVISION\.", " "}) | Set-Content $_
}
UPDATE
I got this to work and it's not pretty.
The only problem is is is capturing the string in the comments section.
What I need to do is only count the string as a hit when it's found starting in position 8 on each line.
Is that possible?
Get-ChildItem 'C:\Thrivent\COBOL_For_EvolveWare\COBOL\COBOL\*.*' -Recurse | ForEach {
($cnt=(Get-Content $_ | select-string -pattern "PROCEDURE DIVISION").length)
if ($cnt -gt "1") {
(Get-Content $_ | ForEach { $_ -replace "PROCEDURE DIVISION\.", " "}) | Set-Content $_
$FileName = $_.FullName
Write-Host "$FileName = $cnt" -foregroundcolor green
}

There are potential issues with all of the provided answers. Reading a file using switch statement is likely going to be the fastest method. But it needs to take into account PROCEDURE DIVISION. appearing multiple times on the same line. The method below will be more memory intensive than using switch but will consider the multi-match, single line condition. Note that you can use -cmatch for case- sensitive matching.
# Matches second occurrence of match when starting in position 7 on a line
Get-ChildItem 'C:\Temp\*.cbl' -Recurse -File | ForEach-Object {
$text = Get-Content -LiteralPath $_.Fullname -Raw
if ($text -match '(?sm)(\A.*?^.{6}PROCEDURE DIVISION\..*?^.{6})PROCEDURE DIVISION\.(.*)\Z') {
Write-Host "Changing file $($_.FullName)"
$matches.1+$matches.2 | Set-Content $_.FullName
}
}

This maybe a bit of a hack, but it works. $myMatches = $pattern.Matches in the case below gives us 3 matches, $myMatches[1].Index is the position of the second occurrence of the string you want to replace.
$text = "Hello foo, where are you foo? I'm here foo."
[regex]$pattern = "foo"
$myMatches = $pattern.Matches($text)
if ($myMatches.count -gt 1)
{
$newtext = $text.Substring(0,$myMatches[1].Index) + "bar" + $text.Substring($myMatches[1].Index + "foo".Length)
$newtext
}

try this:
$Founded=Get-ChildItem 'C:\Temp\' -Recurse -file -Filter "*.cbl" | Select-String -Pattern 'PROCEDURE DIVISION.' -SimpleMatch | where LineNumber -GT 1 | select Path -Unique
$Founded | %{
$Nb=0
$FilePath=$_.Path
$Content=Get-Content $FilePath | %{
if($_ -like '*PROCEDURE DIVISION.*')
{
$Nb++
if ($Nb -gt 1)
{
$_.replace('PROCEDURE DIVISION.', '')
}
else
{
$_
}
}
else
{
$_
}
}
$Content | Set-Content -Path $FilePath
}

You could use switch for this:
Get-ChildItem -Path 'C:\Temp' -Filter '*.cbl' -File -Recurse | ForEach-Object {
$occurrence = 0
$contentChanged = $false
$newContent = switch -Regex -File $_.FullName {
'PROCEDURE DIVISION\.' {
$occurrence++
if ($occurrence -eq 2) {
$_ -replace 'PROCEDURE DIVISION\.', " "
$contentChanged = $true
}
else { $_ }
}
default { $_ }
}
# only rewrite the file if a change has been made
if ($contentChanged) {
Write-Host "Updating file '$($_.FullName)'"
$newContent | Set-Content -Path $_.FullName -Force
}
}

Related

select-string with multiple conditions with powershell

I'm looking for a way to find 2 different lines in a file and only if those 2 line exist I need to perform a task.
So far this is my code
$folderPath = c:\test
$files = Get-ChildItem $Folderpath -Filter *.txt
$find = 'stringA'
$find2 = 'StringB'
$replace = 'something to replace with string b'
if ($files.Length -gt 0 ) {
$files |
select -ExpandProperty fullname |
foreach {
If(Select-String -Path $_ -pattern $find , $find2 -quiet )
{
(Get-Content $_) |
ForEach-Object {$_ -replace $find2, $replace } |
Set-Content $_
write-host "File Changed : " $_
}
}
}
else {
write-host "no files changed"
}
Currently if I run it once it will change the files but if I run it again it will also notify me that it changed the same files instead of the output "no files changed"
Is there a simpler way to make it happen?
Thanks
The Select-String cmdlet selects lines matching any of the patterns supplied to it. This means that the following file contains a match:
PS> Get-Content file.txt
This file contains only stringA
PS> Select-String -Pattern 'stringA', 'stringB' -Path file.txt
file.txt:1:This file contains only stringA
Passing the -Quiet flag to Select-String will produce a boolean result instead of a list of matches. The result is $True even though only one of the patterns is present.
PS> Get-Content file.txt
This file contains only stringA
PS> Select-String -Pattern 'stringA', 'stringB' -Path file.txt -Quiet
True
In your case, Select-String chooses all the files containing either 'stringA' or 'stringB', then replaces all instances of 'stringB' in those files. (Note that replacements are also performed in files you did not want to alter)
Even after the replacements, files containing only 'stringA' still exist: these files are caught and reported by your script the second time you run it.
One solution is to have two separate conditions joined by the -and operator:
If (
(Select-String -Path $_ -Pattern 'stringA' -Quiet) -and
(Select-String -Path $_ -Pattern 'stringB' -Quiet)
)
After this the script should work as intended, except that it won't report "no files changed" correctly.
If you fix your indentation you'll realise that the final else clause actually checks if there are no .txt files in the folder:
$files = Get-ChildItem $Folderpath -Filter *.txt
...
if ($files.length -gt 0) {
...
} else {
# will only display when there are no text files in the folder!
Write-Host "no files changed"
}
The way to resolve this would be to have a separate counter variable that increments every time you find a match. Then at the end, check if this counter is 0 and call Write-Host accordingly.
$counter = 0
...
foreach {
if ((Select-String ...) ...) {
...
$counter += 1
}
}
if ($counter -eq 0) {
Write-Host "no files changed"
}
To complement equatorialsnowfall's helpful answer, which explains the problem with your approach well, with a streamlined, potentially more efficient solution:
$folderPath = c:\test
$searchStrings = 'stringA', 'stringB'
$replace = 'something to replace string B with'
$countModified = 0
Get-ChildItem $Folderpath -Filter *.txt | ForEach-Object {
if (
(
($_ | Select-String -Pattern $searchStrings).Pattern | Select-Object -Unique
).Count -eq $searchStrings.Count
) {
($_ | Get-Content -Raw) -replace $searchStrings[1], $replace |
Set-Content $_.FullName
++$countModified
write-host "File Changed: " $_
}
}
if ($countModified -eq 0) {
Write-Host "no files changed"
}
A single Select-String call is used to determine if all pattern match (the solution scales to any number of patterns):
Each Microsoft.PowerShell.Commands.MatchInfo output object has a .Pattern property that indicates which of the patterns passed to -Pattern matched on a given line.
If, after removing duplicates with Select-Object -Unique, the number of patterns associated with matching lines is the same as the number of input patterns, you can assume that all input patterns matched (at least once).
Reading each matching file as a whole with Get-Content's -Raw switch and therefore performing only a single -replace operation per file is much faster than line-by-line processing.

Powershell Foreach construction remove text in multiple files

As newby to powershell I googled a long time to make a piece that can remove a part of text in a single file based upon some variables like this. The text section can be anywhere within the file. And that specific section needs to be removed.
<#
#create test file
$file = "c:\users\public\Test\remove.txt"
Set-Content $file (1..100) -Force
$content = Get-Content $file
#>
$file = Get-Content "c:\users\public\Test\remove.txt"
$StartText= '50'
$EndText= "75"
$LineNumberStart= $file | Select-string -Pattern $StartText
$Start = $linenumberStart.LineNumber
$LineNumberEnd= $file | Select-string -Pattern $EndText
$End = $linenumberEnd.LineNumber
$keep = $file[0..$Start] + $file[$End..($file.Count - 1)]
$keep | Set-Content "c:\users\public\test\remove.txt"
Now I would like have to the above working functionality but on all files in a particular folder. However, for the reason "Expressions are only allowed as the first element of a pipeline." I can't get it to work with this piece of code below:
$ReportPath = Split-Path -Parent $Script:MyInvocation.MyCommand.Path
$StartText= "50"
$EndText= "75"
Get-ChildItem -Path $ReportPath -Filter *.txt | ForEach { (Get-Content $_.PSPath) |
$LineNumberStart= $_ | Select-string -Pattern $StartText
$Start = $LineNumberStart.LineNumber
$LineNumberEnd= $_ | Select-string -Pattern $EndText
$End = $LineNumberEnd.LineNumber
$keep = $_[0..$Start] + $_[$End..($_.Count - 1)]
$keep|Set-Content $_.PSPath
}
Expected outcome is that all files in the folder have the middle section of the text file removed.
Can someone please assist in getting this foreach construction resolved?
I would recommend you to use a switch for this use case, if I understand correctly you're looking to start skipping the lines of a file from where the line matches the value of $StartText and stop skipping after the line matches the value of $EndText. If that's the case, here is a minimal example of how you can do this using a switch statement with the -Regex parameter:
$StartText = '50'
$EndText = '75'
$skip = $false
switch -Regex (0..100) {
$StartText { $skip = $true; continue }
$EndText { $skip = $false; continue }
{ $skip } { continue }
default { $_ }
}
If this is what you expected, then if you want to the same for each file, the code would look like this, note the use of -File to read each file content:
$StartText = '50'
$EndText = '75'
$skip = $false
Get-ChildItem -Path $ReportPath -Filter *.txt | ForEach-Object {
& {
switch -Regex -File ($_.FullName) {
$StartText { $skip = $true; continue }
$EndText { $skip = $false; continue }
{ $skip } { continue }
default { $_ }
}
} | Set-Content ($_.BaseName + '-New' + $_.Extension)
}

how to replace one line with two lines in txt file

I want some .cs model file to append annotation. If script finds specific property it will put above that property annotation.
Here is the script:
$annotation = "[DatabaseGenerated(DatabaseGeneratedOption.Computed)]"
Get-ChildItem -Filter *.cs | % {
(Get-Content $_.FullName) | ForEach-Object {
if ($_ -match "StartDateTime") {
$_ -replace $_ , "`n`t`t$annotation`n$_"
}
} | Set-Content $_.FullName
}
It works with replacing, but at the end I get a blank file with only two lines (annotation and custom property). I realize that the last pipeline Set-Content $_.FullName is messed up.
If I remove Set-Content, nothing happens with my file (it's not updated)?
This should work better for you:
$filePath = '<YOUR PATH HERE>'
$annotation = "[DatabaseGenerated(DatabaseGeneratedOption.Computed)]"
Get-ChildItem -Path $filePath -Filter *.cs | ForEach-Object {
$file = $_.FullName
(Get-Content $file) | ForEach-Object {
# test all strings in $file
if ($_ -match "StartDateTime") {
# emit the annotation followed by the string itself
"`r`n`t`t$annotation`r`n" + $_
}
else {
# just output the line as-is
$_
}
} | Set-Content -Path $file -Force
}
Within the Foreach-Object I'm capturing the $_.FullName for later use and also to not confuse it with the $_ you use later on as line in the file.
Then, if the line does match the if, output the replaced line, but if it does not (in the else) you should output the line unchanged.
Then, the Set-Content always outputs each line, replaced or not.
Since you actually are not replacing anything inside the string, but rather prefixing it with an annotation, this can be simplified a bit like so:
$annotation = "[DatabaseGenerated(DatabaseGeneratedOption.Computed)]"
Get-ChildItem -Path 'D:\' -Filter *.cs | ForEach-Object {
$file = $_.FullName
(Get-Content $file) | ForEach-Object {
# test all strings in $file
if ($_ -match "StartDateTime") {
# emit the annotation
"`r`n`t`t$annotation"
}
# output the line as-is
$_
} | Set-Content -Path $file -Force
}

Filter lines according to word by powershell

I want to filter lines according to specific word from file in powershell.
For example: the files animal1.txt and animal2.txt. Every file contain lines
dog
cat
dog
dog
bird
Then I want to create two derived files:
animal1_bak.txt that stores lines which contains the word 'dog' from animal1.txt
animal2_bak.txt that stores lines which contains the word 'dog' from animal2.txt
What I found on web is:
Select-String -Path "*.*" -Pattern "dog"
But the instruction to create the derived word is missing.
What can I do?
You can first get-content and use set-content like below
Get-Content -Path E:\KTDocs\Scripts\animal1.txt | where {
$_ -like '*dog*'} |Set-Content e:\animalbak.txt
try Something like this
select-string -Path "c:\temp\animal*.txt" -Pattern "dog" | Group Path | %{
$FileName="{0}_bak.txt" -f $_.Name
$_.Group.Line | select -unique | Out-File $FileName -Append
}
$folderpath = "D:\AnimalFolder" # your folder path here
$Allfiles = Get-ChildItem -Path $folderpath -Recurse -File -Force -ErrorAction SilentlyContinue |where{$_.Name -match ".txt"} |Select-Object -expandproperty FullName
foreach($filepath in $allfiles)
{
$Data = get-content $filepath
foreach($line in $data)
{
if($line -match "dog")
{
$newpath = $filepath.split('.')[0]
$getfullpath = $newpath + "_bak.txt"
$line | out-file $getfullpath -append
}
}
}

Matching Lines in a text file based on values in CSV

Hi Everyone,
I am having trouble with the below script. Here is the requirement:
1) Each text file needs to be compared with a single CSV file. The CSV file contains the data to that if present in the text file should match.
2) If the data in the text file matches, output the matches only and run jobs etc..
3) If the text file has no matches to the CSV file, exit with 0 as no matches are found.
I have tried to do this, but what I end up with is matches, and also non matches. What I really need is to match the lines, run the jobs,exit, if text file has no matches, then return 0
$CSVFIL = Import-Csv -Path $DRIVE\test\csvfile.csv
$TEXTFIL = Get-Content -Path "$TEXTFILFOL\*.txt" |
Select-String -Pattern 'PAT1' |
Select-String -Pattern 'PAT2' |
Select-String -Pattern 'TEST'
ForEach ($line in $CSVFIL) {
If ($TEXTFIL -match $line.COL1) {
Write-Host 'RUNNING:' ($line.JOB01)
} else {
write-host "No Matches Found Exiting"
I would handle this a different way. First you need to find matches, if there are matches then process else output 0.
$matches = #()
foreach ($line in $CSVFIL)
{
if ($TEXTFIL -contains $line.COL1)
{ $matches += $line }
}
if ($matches.Count -gt 0)
{
$matches | Foreach-Object {
Write-Output "Running: $($_.JOB01)"
}
}
else
{
Write-Output "No matches found, exiting"
}
$CSVFIL = Import-Csv -Path "$DRIVE\test\csvfile.csv"
Get-Content -Path "$TEXTFILFOL\*.txt" |
where {$_ -like "*PAT1*" -and $_ -like "*PAT2*" -and $_ -like "*TEST*" } |
%{
$TEXTFOUNDED=$_; $CSVFIL | where {$TEXTFOUNDED -match $_.COL1} |
%{ [pscustomobject]#{Job=$_.JOB01;TextFounded=$TEXTFOUNDED;Col=$_.COL1 } }
}