Powershell - replace line in .txt if condition is met - powershell

i'm quite new to scripting (few weeks) and would be happy about your help.
I've a log-file (.txt) which needs to be changed.
The content is always the same:
random text
random text
successfull
error
random text
random text
random text
error
...
I would like to remove the line containing the word "error", but only if the line above contains the word "successfull".
So far I managed to get all the matching strings out of the File and am able to replace them, but I lose the rest of the text in the process:
get-content "D:\test.txt" | select-string -pattern "error" -context 1,0 | Where-Object {"$_" -match "successfull" } | %{$_ -replace "error.*"} | Out-File "D:\result.txt"
I would really appreciate your help here.

You can use some conditional logic (if statements) to achieve the goal:
$successful = $false
Get-Content d:\test.txt | Foreach-Object {
if ($_ -match "successfull") {
$successful = $true
$_
}
elseif ($_ -match "error" -and $successful) {
$successful = $false
}
else {
$_
$successful = $false
}
}
Since we are piping the Get-Content result into Foreach-Object, $_ becomes the current line being processed (each line is processed one by one). If a line contains successfull, then we mark $successful as $true and still output that line ($_). If the line contains error, then we will only output it if $successful is $false. Anytime we reach a line that does not contain succcesfull, we mark $successful as $false.
No deletion is actually occurring as it is merely not displaying error lines when the conditions are met.

Related

Replace line contents found after specific string

I have multiple text documents, each with multiple lines of information, and I'm trying to replace a single line of text within each document with text of my choosing. The single line of text that is to be replaced does not have a consistent length or set of characters across the multiple documents. Also, the placement of this line of text is not always located at the same place within the document. The only consistent factor here is the string directly above the line of text to be replaced is the same string across all documents - "Courier". I'm trying to use the word "Courier" as my reference point with which I'd replace the subsequent line of text with something of my choosing.
Any help would be greatly appreciated! Thanks in advance!
Below I have included the script that I've created so far; however, I am reaching the limits of my capability to complete this. Currently, the script executes successfully without errors, but the line I'm trying to replace does not get replaced - Instead, the text I'm looking to input as the replacement is entered below "Courier" and the text I don't need (that I'd like to be replaced) is moved down the document, now located directly under the new text I've entered in my script. Here's an example of what I get when I run my script in its current state:
Courier
Entry location 153
Sidewalk0156378
In this case, "Sidewalk0156378" is the old text that used to be directly under "Courier" before the script was ran, and it needs to be replaced. "Entry location 153" is the new text that should be taking the place of "Sidewalk0156378".
$path = "C:\temp"
if(!(Test-Path $path)){
New-Item -ItemType Directory -Force -Path $path
}
$currentCourier = "C:\Temp\currentCourier.txt"
$editCourier = "C:\Temp\editCourier.txt"
$newCourier = "C:\Temp\newCourier.txt"
Get-Content $currentCourier | ForEach-Object {
$_
if ($_ -match 'Courier') {
"Entry location"
}
} | Set-Content $editCourier
$oldEntry = Get-Content $editCourier
$rem = #()
#("Courier") | ForEach-Object {
$rem += $oldEntry[(($oldEntry | Select-String -Pattern "$_").LineNumber)..(($oldEntry | Select-String -Pattern "$_").LineNumber+1)]
}
Compare-Object $oldEntry $rem | Select-Object -ExpandProperty InputObject | Set-Content $newEntry
The following uses a switch to process the file line by line in combination with the -Wildcard parameter to match any line having the key word.
& {
$skipNext = $false
switch -Wildcard -File $currentCourier {
# if line contains Courier
'*Courier*' {
# output this line
$_
# set this variable to skip next line
$skipNext = $true
# output new value that replaces next line
'new value here'
# skip next conditions
continue
}
# if the bool was set in previous iteration
{ $skipNext } {
# set it to false again
$skipNext = $false
# and go next
continue
}
# else, output line as is
Default { $_ }
}
} | Set-Content $newCourier
Using the logic above with a hardcoded example:
$content = '
line 1
line 2
line 3 has Courier
line 4 should be replaced
line 5
' -split '\r?\n'
$skipNext = $false
switch -Wildcard ($content) {
'*Courier*' {
$_
$skipNext = $true
'Entry location 153'
continue
}
{ $skipNext } {
$skipNext = $false
continue
}
Default { $_ }
}
Output to the console would become:
line 1
line 2
line 3 has Courier
Entry location 153
line 5

Is there a way to display the lines of text that meets a condition with PowerShell

$data = Select-String -Path $selectedDirectory\$sqlFile -Pattern "GRANT" -Context 5,5
I want to use PowerShell to read .SQL files and we want to make sure that a user isn't using GRANT or DROP or DELETE without a human reviewing the file to see if it's okay.
My 1 line only is looking at GRANT but I don't think it's working.
If the keywords are in the file, I want to display a portion of the text on the screen +/- 5 lines of where the offending text was found.
Is there a way to change the color of the text for the specific line that has the offending search criteria (all other lines will be shown as default)
If you want colors displayed to the console, you will need to utilize Write-Host.
$data = Select-String -Path $selectedDirectory\$sqlFile -Pattern "GRANT|DROP|DELETE" -Context 5,5
$data | Foreach-Object {
$_.Context.Precontext
Write-Host $_.Line -ForeGroundColor Cyan
$_.Context.Postcontext
}
I'll give it a shot.
This function takes a file, searches for those keywords, and then prints +/- 5 lines. It's easy enough that I'm sure you know how it works and how to modify it. You can find the reference for the matchinfo class (returned by Select-String( here.
Function Get-SQLForbiddenWords ($sqlDataFile) {
$data = Select-String -Path $sqlDataFile -Pattern "GRANT|DROP|DELETE"
Foreach ( $line in $data) {
$lineNumberS = $line.LineNumber - 5
$lineNumberE = $line.LineNumber + 5
echo ('Bad Command Detected: {0}' -f $line.line)
(Get-Content $sqlDataFile)[$lineNumberS..$lineNumberE]
echo "`n"
}
}
It was pretty fun. Output:
Bad Command Detected: DROP
this is the sixth line
GRANT
this is the seventh line
this is the eighth line
DROP
this is the ninth line
this is the tenth linet
this is the eleventh line
this is the twelfbthfbth line
For starters, "GRANT" should be in quotes to denote a string.
If you notice, $line = Select-String -Pattern "Grant" will return an object.
If you look at the properties of the object using Get-Member, one of them is LineNumber
If you have read the contents of your file using $data = Get-Content File.sql or any something similar, you will have your data as an array object. Now you can now use this line number to extract the +/- 5 lines as you wish like $data[50..60]. This will show output lines from 50th to 60th line. You can easily replace the 50 and 60 with your variables.
Another way is to use the oss function (=Out-String -Stream).
Select-String "\b(GRANT|DROP|DELETE)\b" .\test.txt -Context 5 | oss | foreach { Write-Host $_ -ForegroundColor ("White","Cyan")[$_[0] -eq '>'] }
The following may make it a bit more readable.
Select-String "\b(GRANT|DROP|DELETE)\b" .\test.txt -Context 5 | Out-String -Stream | foreach {
if(-not $_) { return }
$fileName,$lineNumber,$line = $_.Split(":", 3)
$color = if($_.StartsWith(">")) { "Cyan" } else { "White" }
Write-Host $fileName $lineNumber.PadLeft(3, "0") $line -ForegroundColor $color -Separator " "
}

Overwrite PowerShell output strings onto the same line

I have a piece of PS code which takes the 7-Zip extraction output and filters it down so only percentage "%" progress update lines get printed. I've managed to reduce it down to just the percentage outputs:
& $7ZipPath "x" $filePath "-o$extractionPath" "-aos" "-bsp1" | out-string -stream | Select-String -Pattern "\d{1,3}%" -AllMatches | ForEach-Object { $_.Matches.Value } | Write-Host -NoNewLine
At the moment the console output looks like this:
0%1%5%9%14%17%20%23%26%31%37%43%46%48%50%52%54%56%59%61%63%65%67%70%72%74%76%78%80%81%82%83%85%86%87%89%90%91%92%94%95%96%97%98%99%
Is there a way of keeping these outputs in the same place, on the same line, making them just overwrite each other? It's tricky because the output is being piped from the 7-Zip application. I'm afraid I can't use Expand-Archive as I am dealing with .7z files
Many thanks!
You could use the .Net System.Console class:
[System.Console]::SetCursorPosition(0, [System.Console]::CursorTop)
So your code would have to be:
& $7ZipPath "x" $filePath "-o$extractionPath" "-aos" "-bsp1" | out-string -stream | Select-String -Pattern "\d{1,3}%" -AllMatches | ForEach-Object { $_.Matches.Value } | foreach {
[System.Console]::SetCursorPosition(0, [System.Console]::CursorTop)
Write-Host $_ -NoNewLine
}
Note: As long as the next output is equal or greater length, which is true in your case, this is all you need. Otherwise you would have to clear the last output first.
marsze's helpful answer works well, but there's a simpler alternative that uses a CR character ("`r") to reset the cursor position to the start of the line.
Here's a simple demonstration that prints the numbers 1 through 10 on the same line:
1..10 | ForEach-Object { Write-Host -NoNewline "`r$_"; Start-Sleep -Milliseconds 100 }
[Console]::Write(...) instead of Write-Host -NoNewline ... works too, as Bacon Bits points out.
The same constraint applies, however: if previous output lines happened to be longer, the extra characters linger.
To solve this problem too, you must pad any output line to the length of the console window's buffer width:
'loooooooong', 'meeedium', 'short' | ForEach-Object {
Write-Host -NoNewline ("`r{0,-$([console]::BufferWidth)}" -f $_)
Start-Sleep -Milliseconds 500
}

Powershell issue with do while loop

I've got a simple bit of code that looks for a string in a series of log files.
If it finds the string, it should exit the loop (nested inside another loop as part of a function) with $buildlogsuccess = 'True'
If it can't find the string, it should exit and return $buildlogsuccess = 'False'
The select-string statement itself works, however it looks like there's something wrong with the below code:
$logArr = gci C:\build\Logs | where {($_.name -like 'install*.log') -and (! $_.PSIsContainer)} | select -expand FullName
$count = ($logArr).count
Foreach ($log in $logArr) {
Do {
$count -= 1
$buildlogsuccess = [bool](select-string -path $log -simplematch $buildstring)
If (($buildlogsuccess)) {break}
} while ($count -gt '0')
}
When one of the logs has the string, the loop finishes and should return $buildlogsuccess as 'True'.
If I check $log it shows the file that I know has the string (in this instance C:\build\Logs\Installer1.log).
Strangely, at this point $count shows as having a value of -1?
If I take the string out of that file and run again it also exits and returns the correct variable value (and shows the $log variable as the last file in $logArr as expected), but this time $count shows as -24.
My code is also returning $buildlogsuccess as 'False' when the string is present in one of the log files.
Re-tested [bool](select-string -path $log -simplematch $buildstring) by manually populating $log (with a file that has that string) and $buildstring and get 'True' as expected when using
[bool](select-string -path $log -simplematch $buildstring)
Note: Variables it uses:
$buildstring = "Package
'F:\xxx\Bootstrap\apackage\Installsomething.xml' processed
successfully"
Any help identifying where I've gone wrong would be appreciated.
Your code can be greatly simplified:
$buildlogsuccess = Select-String -SimpleMatch -Quiet $buildstring C:\build\Logs\install*.log
The above assumes that there are no directories that match install*.log; if there's a chance of that, pipe the output of Get-ChildItem -File C:\build\Logs -Filter install*.log to Select-String instead.
Do-while will first do the thing, then check the while statement. You're iterating over n files. It doesn't check the value of $count before it executes that portion.
So let's say the first file does not contain the string you're looking for. It will (correctly) decrement the $count variable to zero, and then it moves on to the next $log in $logArr.
Now for each next file in the folder it will decrement $count, and then exit the loop when it sees that $count is not greater than 0.
I don't know why you're using the do-while loop at all here
Thanks Norsk
I over-complicated for myself.
This worked:
$logArr = gci C:\build\Logs | where {($_.name -like 'install*.log') -and (! $_.PSIsContainer)} | select -expand FullName
$count = ($logArr).count
Foreach ($log in $logArr) {
$buildlogsuccess = [bool](select-string -path $log -simplematch $buildstring)
If ($buildlogsuccess) {break}
}

How can I search the first line and the last line in a text file?

I need to only search the 1st line and last line in a text file to find a "-" and remove it.
How can I do it?
I tried select-string, but I don't know to find the 1st and last line and only remove "-" from there.
Here is what the text file looks like:
% 01-A247M15 G70
N0001 G30 G17 X-100 Y-100 Z0
N0002 G31 G90 X100 Y100 Z45
N0003 ; --PART NO.: NC-HON.PHX01.COVER-SHOE.DET-1000.050
N0004 ; --TOOL: 8.55 X .3937
N0005 ;
N0006 % 01-A247M15 G70
Something like this?
$1 = Get-Content C:\work\test\01.I
$1 | select-object -index 0, ($1.count-1)
Ok, so after looking at this for a while, I decided there had to be a way to do this with a one liner. Here it is:
(gc "c:\myfile.txt") | % -Begin {$test = (gc "c:\myfile.txt" | select -first 1 -last 1)} -Process {if ( $_ -eq $test[0] -or $_ -eq $test[-1] ) { $_ -replace "-" } else { $_ }} | Set-Content "c:\myfile.txt"
Here is a breakdown of what this is doing:
First, the aliases for those now familiar. I only put them in because the command is long enough as it is, so this helps keep things manageable:
gc means Get-Content
% means Foreach
$_ is for the current pipeline value (this isn't an alias, but I thought I would define it since you said you were new)
Ok, now here is what is happening in this:
(gc "c:\myfile.txt") | --> Gets the content of c:\myfile.txt and sends it down the line
% --> Does a foreach loop (goes through each item in the pipeline individually)
-Begin {$test = (gc "c:\myfile.txt" | select -first 1 -last 1)} --> This is a begin block, it runs everything here before it goes onto the pipeline stuff. It is loading the first and last line of c:\myfile.txt into an array so we can check for first and last items
-Process {if ( $_ -eq $test[0] -or $_ -eq $test[-1] ) --> This runs a check on each item in the pipeline, checking if it's the first or the last item in the file
{ $_ -replace "-" } else { $_ } --> if it's the first or last, it does the replacement, if it's not, it just leaves it alone
| Set-Content "c:\myfile.txt" --> This puts the new values back into the file.
Please see the following sites for more information on each of these items:
Get-Content uses
Get-Content definition
Foreach
The Pipeline
Begin and Process part of the Foreach (this are usually for custom function, but they work in the foreach loop as well)
If ... else statements
Set-Content
So I was thinking about what if you wanted to do this to many files, or wanted to do this often. I decided to make a function that does what you are asking. Here is the function:
function Replace-FirstLast {
[CmdletBinding()]
param(
[Parameter( `
Position=0, `
Mandatory=$true)]
[String]$File,
[Parameter( `
Position=1, `
Mandatory=$true)]
[ValidateNotNull()]
[regex]$Regex,
[Parameter( `
position=2, `
Mandatory=$false)]
[string]$ReplaceWith=""
)
Begin {
$lines = Get-Content $File
} #end begin
Process {
foreach ($line in $lines) {
if ( $line -eq $lines[0] ) {
$lines[0] = $line -replace $Regex,$ReplaceWith
} #end if
if ( $line -eq $lines[-1] ) {
$lines[-1] = $line -replace $Regex,$ReplaceWith
}
} #end foreach
}#End process
end {
$lines | Set-Content $File
}#end end
} #end function
This will create a command called Replace-FirstLast. It would be called like this:
Replace-FirstLast -File "C:\myfiles.txt" -Regex "-" -ReplaceWith "NewText"
The -Replacewith is optional, if it is blank it will just remove (default value of ""). The -Regex is looking for a regular expression to match your command. For information on placing this into your profile check this article
Please note: If you file is very large (several GBs), this isn't the best solution. This would cause the whole file to live in memory, which could potentially cause other issues.
try:
$txt = get-content c:\myfile.txt
$txt[0] = $txt[0] -replace '-'
$txt[$txt.length - 1 ] = $txt[$txt.length - 1 ] -replace '-'
$txt | set-content c:\myfile.txt
You can use the select-object cmdlet to help you with this, since get-content basically spits out a text file as one huge array.
Thus, you can do something like this
get-content "path_to_my_awesome_file" | select -first 1 -last 1
To remove the dash after that, you can use the -Replace switch to find the dash and remove it. This is better than using System.String.Replace(...) method because it can match regex statements and replace whole arrays of strings too!
That would look like:
# gc = Get-Content. The parens tell Powershell to do whatever's inside of it
# then treat it like a variable.
(gc "path_to_my_awesome_file" | select -first 1 -last 1) -Replace '-',''
If your file is very large you might not want to read the whole file to get the last line. gc -Tail will get the last line very quickly for you.
function GetFirstAndLastLine($path){
return New-Object PSObject -Property #{
First = Get-Content $path -TotalCount 1
Last = Get-Content $path -Tail 1
}
}
GetFirstAndLastLine "u_ex150417.log"
I tried this on a 20 gb log file and it returned immediately. Reading the file takes hours.
You will still need to read the file if you want to keep all excising content and you want only to remove from the end. Using the -Tail is a quick way to check if it is there.
I hope it helps.
A cleaner answer to the above:
$Line_number_were_on = 0
$Awesome_file = Get-Content "path_to_ridiculously_excellent_file" | %{
$Line = $_
if ($Line_number_were_on -eq $Awesome_file.Length)
{ $Line -Replace '-','' }
else
{ $Line } ;
$Line_number_were_on++
}
I like one-liners, but I find that readability tends to suffer sometimes when I put terseness over function. If what you're doing is going to be part of a script that other people will be reading/maintaining, readability might be something to consider.
Following Nick's answer: I do need to do this on all text files in the directory tree and this is what I'm using now:
Get-ChildItem -Path "c:\work\test" -Filter *.i | where { !$_.PSIsContainer } | % {
$txt = Get-Content $_.FullName;
$txt[0] = $txt[0] -replace '-';
$txt[$txt.length - 1 ] = $txt[$txt.length - 1 ] -replace '-';
$txt | Set-Content $_.FullName
}
and it looks like it's working well now.
Simple process:
Replace $file.txt with your filename
Get-Content $file_txt | Select-Object -last 1
I was recently searching for comments in the last line of .bat files. It seems to mess up the error code of previous commands. I found this useful for searching for a pattern in the last line of files. Pspath is a hidden property that get-content outputs. If I used select-string, I would lose the filename. *.bat gets passed as -filter for speed.
get-childitem -recurse . *.bat | get-content -tail 1 | where { $_ -match 'rem' } |
select pspath
PSPath
------
Microsoft.PowerShell.Core\FileSystem::C:\users\js\foo\file.bat