Restructure text file using Powershell - powershell

I have an text file that is structured in the format below, I need to extract the Hostname, Stratum=x and Offset=y values into a structured format e.g. CSV. I intend to use the output to write windows event logs if the values meet certain thresholds. My thinking is that creating objects (e.g. the hostnames) and adding the stratum and offset values as members would allow me to achieve this, but my PowerShell skills fail me here..
___________________________________________________________________________
02/04/2020 08:11:00 : Started [TEST] Command Scrape
Text I don't care about
___________________________________________________________________________
Hostname_1 (192.168.1.254):
assID=0 status=0544 leap_none, sync_local_proto, 4 events, event_peer/strat_chg,
version="ntpd 4.2.2p1#1.1570-o Tue May 19 13:57:55 UTC 2009 (1)",
processor="x86_64", system="Linux/2.6.18-164.el5", leap=00, stratum=4,
precision=-10, rootdelay=0.000, rootdispersion=11.974, peer=59475,
refid=LOCAL(0),
reftime=d495c32c.0e71eaf2 Mon, Jan 7 2013 13:57:00.056, poll=10,
clock=d495c32c.cebd43bd Mon, Jan 7 2013 13:57:00.807, state=4,
offset=0.123, frequency=0.000, jitter=0.977, noise=0.977,
stability=0.000, tai=0
___________________________________________________________________________
Hostname_2 (10.10.1.1):
assID=0 status=0544 leap_none, sync_local_proto, 4 events, event_peer/strat_chg,
version="ntpd 4.2.2p1#1.1570-o Tue May 19 13:57:55 UTC 2009 (1)",
processor="x86_64", system="Linux/2.6.18-164.el5", leap=00, stratum=4,
precision=-10, rootdelay=0.000, rootdispersion=11.974, peer=59475,
refid=LOCAL(0),
reftime=d495c32c.0e71eaf2 Mon, Jan 7 2013 13:57:00.056, poll=10,
clock=d495c32c.cebd43bd Mon, Jan 7 2013 13:57:00.807, state=4,
offset=2.456, frequency=0.000, jitter=0.977, noise=0.977,
stability=0.000, tai=0
___________________________________________________________________________
Hostname_3 (10.10.1.2):
...
I found I can create the CSV if I manually reformat the data into keypairs (as below), using ConvertFrom-StringData and outputting to CSV;
(Get-Content 'file.txt' -Raw) -split '####' |
ForEach-Object {
$results = Convertform-StringData - StringData ($PSitem -replace '\n-\s+')
New-Object PSObject -Property $results | Select-Object Hostname, Stratum, Offset
} | Export-Csv 'file.csv' - NoTypeInformation
Hostname=Hostname_1
stratum=3
offset=-10.345
####
Hostname=Hostname_2
stratum=4
offset=-8.345
Becomes the following CSV:
"Hostname","Stratum","offset"
"Hostname_1","3","-10.345"
"Hostname_2","4","-8.345"

You could do this with the code below.
In your example, the text blocks are separated by a series of underscores. If in real life that is different, change the -split '_{2,}' accordingly.
$regex = '(?s)^\s*([\w_\d]+)\s+\(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\):.*stratum=(\d+).*offset=([\d.]+)'
$result = (Get-Content 'D:\file.txt' -Raw) -split '_{2,}' | Where-Object {$_ -match $regex} | ForEach-Object {
[PsCustomObject]#{
'Hostname' = $matches[1]
'Stratum' = $matches[2]
'Offset' = $matches[3]
}
}
# output to console
$result
#output to csv file
$result | Export-Csv -Path 'D:\file.csv' -NoTypeInformation
Output on screen:
Hostname Stratum Offset
-------- ------- ------
Hostname_1 4 0.123
Hostname_2 4 2.456
Output as CSV:
"Hostname","Stratum","Offset"
"Hostname_1","4","0.123"
"Hostname_2","4","2.456"
Regex details:
^ Assert position at the beginning of the string
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
( Match the regular expression below and capture its match into backreference number 1
[\w_\d] Match a single character present in the list below
A word character (letters, digits, etc.)
The character “_”
A single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
\( Match the character “(” literally
\d Match a single digit 0..9
{1,3} Between one and 3 times, as many times as possible, giving back as needed (greedy)
\. Match the character “.” literally
\d Match a single digit 0..9
{1,3} Between one and 3 times, as many times as possible, giving back as needed (greedy)
\. Match the character “.” literally
\d Match a single digit 0..9
{1,3} Between one and 3 times, as many times as possible, giving back as needed (greedy)
\. Match the character “.” literally
\d Match a single digit 0..9
{1,3} Between one and 3 times, as many times as possible, giving back as needed (greedy)
\) Match the character “)” literally
: Match the character “:” literally
. Match any single character
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
stratum= Match the characters “stratum=” literally
( Match the regular expression below and capture its match into backreference number 2
\d Match a single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)
. Match any single character
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
offset= Match the characters “offset=” literally
( Match the regular expression below and capture its match into backreference number 3
[\d.] Match a single character present in the list below
A single digit 0..9
The character “.”
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)

Fantastic, thank you so much. I've made the following adjustments to accommodate non-word characters in the hostname and offset, and exclude the trailing comma on the offset values.
$regex = '(?s)^\s*([\w_\d]+)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):.*stratum=(\d+).*offset=([\d.]+)'
is now:
(?s)^\s*([\w|\W|\d]+)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):.*stratum=(\d+).*offset=([\W|\d.][^,]

Related

How to get tv show episode and session number from title

I'm try to get tvhsow season and episode number from the title
I try following code which works but it also picking up title like xxxxe 3 as episode 3
$episode = $title | Select-String -Pattern "E(\d+)", "E (\d+)", "Episode (\d+)" | % {$_.Matches.Groups[1].Value}
$season = $title | Select-String -Pattern "S(\d+)", "S (\d+)", "Season (\d+)" | % {$_.Matches.Groups[1].Value}
How to i make sure that I can pick up the season number and episode from any of these formats.
xxx S01E01
xxxe 1 S01E01
xxx S01 E01
xxx 01x01
xxx Season 01 Episode 01
If above seaon or episode numbers are not in the title of the show then I just want to return nothing e.g. if the show is named as "xxxxxE 1"
Assuming the following
Seasons and episodes will always be 2 (or more) numbers
Seasons and episodes will always be at the end of the filename.
I would recommend anchoring to the end of the name with the regex pattern. From there we account for 0 or more characters before a period (file extension), 1 literal period (for the extension), 0 or more characters between the period and the episode, and 0 or more characters between the season and the episode.
$examples = #'
xxx S01E02.avi
xxxe 1 S02E03.mp3
xxx S03 E04.mov
xxx 04x05.png
xxx Season 05 Episode 06.wav
'# -split [environment]::NewLine
$examples | ForEach-Object {
if($_ -match '.+(\d{2,}).*(\d{2,}).*\..*$'){
"Season: {0} Episode: {1}" -f $matches.1,$matches.2
}
}
This will output
Season: 01 Episode: 02
Season: 02 Episode: 03
Season: 03 Episode: 04
Season: 04 Episode: 05
Season: 05 Episode: 06
You didn't show how you populated $title, so it was assumed to just be a string. However if you wanted to apply to file objects, you have a couple of options.
We can leave the regex pattern alone and use the Name property.
$videolist = Get-Childitem -Path path\to\movies -Filter *.whatever
foreach($video in $videolist){
if($video.Name -match '.+(\d{2,}).*(\d{2,}).*\..*$'){
"Season: {0} Episode: {1}" -f $matches.1,$matches.2
}
}
or
We can use the BaseName property and adjust the regex slightly.
$videolist = Get-Childitem -Path path\to\movies -Filter *.whatever
foreach($video in $videolist){
if($video.BaseName -match '.+(\d{2,}).*(\d{2,}).*$'){
"Season: {0} Episode: {1}" -f $matches.1,$matches.2
}
}
You could construct a regex string that parses out the season and episode numbers like this:
$examples = 'xxx S01E01','xxxe 1 S01E03','xxx S06 E01','xxx 01x01','xxx Season 01 Episode 02'
foreach ($title in $examples) {
if ($title -match '(?:(?:S(?:eason)?)?\s*(\d+)[\sx]*)(?:(?:E(?:pisode)?)?\s*(\d+))') {
$season = [int]$matches[1]
$episode = [int]$matches[2]
# just to display the output:
[PsCustomObject]#{
Title = $title
Season = $season
Episode = $episode
}
}
}
Output:
Title Season Episode
----- ------ -------
xxx S01E01 1 1
xxxe 1 S01E03 1 3
xxx S06 E01 6 1
xxx 01x01 1 1
xxx Season 01 Episode 02 1 2
Regex details:
(?: # Match the regular expression below
(?: # Match the regular expression below
S # Match the character “S” literally
(?: # Match the regular expression below
eason # Match the characters “eason” literally
)? # Between zero and one times, as many times as possible, giving back as needed (greedy)
)? # Between zero and one times, as many times as possible, giving back as needed (greedy)
\s # Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
* # Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
( # Match the regular expression below and capture its match into backreference number 1
\d # Match a single digit 0..9
+ # Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)
[\sx] # Match a single character present in the list below
# A whitespace character (spaces, tabs, line breaks, etc.)
# The character “x”
? # Between zero and one times, as many times as possible, giving back as needed (greedy)
)
(?: # Match the regular expression below
(?: # Match the regular expression below
E # Match the character “E” literally
(?: # Match the regular expression below
pisode # Match the characters “pisode” literally
)? # Between zero and one times, as many times as possible, giving back as needed (greedy)
)? # Between zero and one times, as many times as possible, giving back as needed (greedy)
\s # Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
* # Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
( # Match the regular expression below and capture its match into backreference number 2
\d # Match a single digit 0..9
+ # Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)
)
I have changed some of your examples to make it clearer the numbers are correctly found

i am trying to Query internet speeds in my lan with a script that send me an alert only if its lower then something ( see description)

i have a code which works fine . i want it to send a different results instead the ones i already have .
Code :
function SpeedTest{}
C:\Users\yigal\Desktop\Tester\speedtest.exe > C:\Users\yigal\Desktop\Results\file.txt
Start-Sleep -s 45
function Send-Email() {
param(
[Parameter(mandatory=$true)][string]$To
)
$username = (Get-Content -Path 'C:\credentials.txt')[0]
$password = (Get-Content -Path 'C:\credentials.txt')[1]
$secstr = New-Object -TypeName System.Security.SecureString
$password.ToCharArray() | ForEach-Object {$secstr.AppendChar($_)}
$hash = #{
from = $username
to = $To
smtpserver = "smtp.gmail.com"
subject = Get-Date
body = (Get-Content -Path 'C:\Users\yigal\Desktop\Results\file.txt')[7] + (Get-Content -Path 'C:\Users\yigal\Desktop\Results\file.txt')[9]
credential = New-Object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr
usessl = $true
verbose = $true
}
Send-MailMessage #hash
}
Start-Sleep -s 45
Send-Email -To "Fake#gmail.com"
This script sends its output to my mail via smtp .
I want it to send me a mail if a certain fields the download and upload speeds are for example lower then 50 (down) and 5 (up) .
my output from the Speedtest.exe script is shown like this :
"
Speedtest by Ookla
Server: ******** - ******** (id = ********)
ISP: ********
Latency: 13.18 ms (2.14 ms jitter)
Download: 30.47 Mbps (data used: 27.1 MB)
Upload: 10.04 Mbps (data used: 11.7 MB)
Packet Loss: 0.0%
Result URL: https://www.speedtest.net/result/c/********
"
i want to query the download and upload speeds from the speedtest.exe output.
something like :
if ($DownloadSpeed < 50 && $UploadSpeed <5 )
{
Send-Email -To "Fake#gmail.com" -Body False
}
I know i need to declare $body but its irrelevant for the IF i am trying to build.
Continuing from my comment, you need to read up on PowerShell operators, because in your code attempt you're using C# style operators.
Next, you use your function Send-Email with a parameter -Body which is not defined there.
Instead, add parameters for the measured Dowload and Upload speed:
function Send-Email {
param(
[Parameter(mandatory=$true)][string]$To
[Parameter(mandatory=$true)][double]$DownloadSpeed
[Parameter(mandatory=$true)][double]$UploadSpeed
)
# rest of the code (I didn't check if that works or not..)
}
To parse out the speed values from the results file, you could do this:
# read the results written by the speedtest.exe as single multi-line string (use -Raw)
$speedResult = Get-Content -Path 'C:\Users\yigal\Desktop\Results\file.txt' -Raw
# test if we can parse out the values for down- and upload speed
if ($speedResult -match '(?s)Download:\s*(?<down>\d+(?:\.\d+)?) Mbps.*Upload:\s*(?<up>\d+(?:\.\d+)?) Mbps') {
$downloadSpeed = [double]$matches['down']
$uploadSpeed = [double]$matches['up']
# if down speed less than 50 AND up speed less than 5
if ($downloadSpeed -lt 50 -and $uploadSpeed -lt 5) {
# send your email here.
Send-Email -To "Fake#gmail.com" -DownloadSpeed $downloadSpeed -UploadSpeed $uploadSpeed
}
}
Regex details:
(?s) The dot also matches newline characters
Download: Match the characters “Download:” literally
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
(?<down> Match the regular expression below and capture its match into backreference with name “down”
\d Match a single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
(?: Match the regular expression below
\. Match the character “.” literally
\d Match a single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)? Between zero and one times, as many times as possible, giving back as needed (greedy)
)
\ Mbps Match the characters “ Mbps” literally
. Match any single character
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
Upload: Match the characters “Upload:” literally
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
(?<up> Match the regular expression below and capture its match into backreference with name “up”
\d Match a single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
(?: Match the regular expression below
\. Match the character “.” literally
\d Match a single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)? Between zero and one times, as many times as possible, giving back as needed (greedy)
)
\ Mbps Match the characters “ Mbps” literally

Insert comma before a value if it is blank

Below is the data I have
-Ignored:31,Modified,src,data,all,*file,MINOSFIC/UTMNUP10
-Ignored:33,Modified,src,&,tgt,data,all,*file,MINOSFIC/UVEGAP10
-Ignored:92,Synchro,is,running,*file,MINOSFIC/VM010P50
-Ignored:01,Object,hold,(synchro),*file,MINOSFIC/VM010U51
here I am parsing the data and keeping in csv
for 1st and 2nd line it is working but when it is coming to 3rd and 4th line, it is pushing the column value one forward as there is no data before *file
Please let me know how to handle this. how to insert a comma before *file if there is not entry (like for first 2 line it is all)
$allIGfiles = Get-ChildItem -Path 'C:\LG2' -Recurse -Filter "*LG_VFN*"
foreach($file in $allIGfiles)
{
$filename = $file.FullName
$data = Get-Content $filename | Select -SkipLast 1
$Lines = #()
foreach ($line in $data){
if($line -match "Ignored")
{
$Lines+=$line
}
}
$NewLines = #($Lines | % { ($_ -replace "\s{2,}",",") -replace "(\d) ", '$1,'} )
$NewLines | Export-Csv 'c:\file.csv' -append -NoTypeInformation
Original data
-Ignored:31 Modified src data all *file MINOSFIC/UTMNUP10
-Ignored:33 Modified src & tgt data all *file MINOSFIC/UVEGAP10
-Ignored:92 Synchro is running *file MINOSFIC/VM010P50
-Ignored:01 Object hold (synchro) *file MINOSFIC/VM010U51
Update:
I am now getting the data like below but when i am trying to put it in csv it is only writing numbers to the file
-Ignored:31,Modified src data,all *file,MINOSFIC/UTMNUP10
-Ignored:33,Modified src & tgt data,all *file,MINOSFIC/UVEGAP10
-Ignored:92,Synchro is running,*file,MINOSFIC/VM010P50
-Ignored:01,Object hold (synchro),*file,MINOSFIC/VM010U51
-Ignored:01,Object hold (synchro),*file,MINOSFIC/VM010U52
-Ignored:01,Object hold (synchro),*file,MINOSFIC/VM010U53
-Ignored:01,Object hold (synchro),*file,MINOSFIC/VM010U54
Data with other code
Object OK (partial) 97% *file MINOSFIC/VM011P10
-Ignored:18 Object hold *file MINOSFIC/VM011P50
Object OK (partial) 78% *file MINOSFIC/VM800P30
*Error: 09 Diff. Creation date *file MINOSSVG/S100000702
*Error: 09 Diff. Creation date *file MINOSSVG/S100000805
-Ignored:18 Object hold *file MINOSSVG/S100001154
*Error: 09 Diff. Creation date *file MINOSSVG/S100001227
You could do this by using a regex that at first ignores the all value, but when constructing the comma separated new string, this will be inserted when found:
Read the file as string array
$data = Get-Content -Path $filename
I'm faking that by using a Here-String below:
$data = #"
-Ignored:31 Modified src data all *file MINOSFIC/UTMNUP10
-Ignored:33 Modified src & tgt data all *file MINOSFIC/UVEGAP10
-Ignored:92 Synchro is running *file MINOSFIC/VM010P50
-Ignored:01 Object hold (synchro) *file MINOSFIC/VM010U51
"# -split '\r?\n'
$result = foreach ($line in $data) {
if ($line -match '^(-Ignored:\d+)\s+(.+)\s+(\*file)\s+(.*)') {
'{0},{1},{2},{3},{4}' -f $matches[1],
($matches[2] -replace 'all$').Trim(),
($matches[2] -split '\s{2,}')[-1],
$matches[3],
$matches[4]
}
}
# output to console screen
$result
# write to file
$result | Set-Content -Path 'X:\TheNewFile.txt'
Output:
-Ignored:31,Modified src data,all,*file,MINOSFIC/UTMNUP10
-Ignored:33,Modified src & tgt data,all,*file,MINOSFIC/UVEGAP10
-Ignored:92,Synchro is running,,*file,MINOSFIC/VM010P50
-Ignored:01,Object hold (synchro),,*file,MINOSFIC/VM010U51
To also do this with *Error.. lines as in your updated example, change the line
if ($line -match '^(-Ignored:\d+)\s+(.+)\s+(\*file)\s+(.*)') {
into
if ($line -match '^((?:-Ignored|\*Error):\s*\d+)\s+(.+)\s+(\*file)\s+(.*)') {
Regex details:
^ Assert position at the beginning of a line (at beginning of the string or after a line break character)
( Match the regular expression below and capture its match into backreference number 1
(?: Match the regular expression below
Match either the regular expression below (attempting the next alternative only if this one fails)
-Ignored Match the characters “-Ignored” literally
| Or match regular expression number 2 below (the entire group fails if this one fails to match)
\* Match the character “*” literally
Error Match the characters “Error” literally
)
: Match the character “:” literally
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
\d Match a single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
( Match the regular expression below and capture its match into backreference number 2
. Match any single character that is not a line break character
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
)
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
( Match the regular expression below and capture its match into backreference number 3
\* Match the character “*” literally
file Match the characters “file” literally
)
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
( Match the regular expression below and capture its match into backreference number 4
. Match any single character that is not a line break character
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
)
One approach is to first replace two consecutive spaces with a comma. Then replace digit followed with a space with the same digit via capture group and a comma. Like so,
$data=#(
'-Ignored:31 Modified src data all *file MINOSFIC/UTMNUP10',
'-Ignored:33 Modified src & tgt data all *file MINOSFIC/UVEGAP10',
'-Ignored:92 Synchro is running *file MINOSFIC/VM010P50',
'-Ignored:01 Object hold (synchro) *file MINOSFIC/VM010U51')
$data | % { ($_ -replace "\s{2,}",",") -replace "(\d) ", '$1,'}
-Ignored:31,Modified src data,all *file,MINOSFIC/UTMNUP10
-Ignored:33,Modified src & tgt data,all *file,MINOSFIC/UVEGAP10
-Ignored:92,Synchro is running,*file,MINOSFIC/VM010P50
-Ignored:01,Object hold (synchro),*file,MINOSFIC/VM010U51
This would get all *file in same column as *file. Should that not be enough, use ConvertFrom-String or do another replacement to introduce the missing column. As of how, you probably need to calculate how many commas there are and deduct from that if the column is needed.

Read a file, count delimiters and output line number with mismatched delimiter

I have a file named: test_file.txt. The second line has 4 pipe delimiters and all other lines except 2nd line has 3 pipe delimiters.
I just want to output line 2 since it has one more delimiter than other lines.
$colCnt = "C:\test.txt"
[int]$LastSplitCount = $Null
Get-Content $colCnt | ?{$_} | Select -Skip 1 | %{if($LastSplitCount -and !
($_.split("|").Count -eq $LastSplitCount))
{"Process stopped at line number $($_.psobject.Properties.value[5]) for column count mis-match.";break}
elseif(!$LastSplitCount){$LastSplitCount = $_.split("|").Count}}
If your text file looks anything like this:
blah|using|three|delimiters
blah|using|four |delimiter |characters
blah|using|three|delimiters
blah|using|four |delimiter |characters
blah|using two |delimiters
The the following code should output the lines with more (or less) than 3 | delimiters:
$line = 0
switch -Regex -File "C:\test.txt" {
'^(?:[^|]*\|){3}[^|]*$' { $line++ } # this line is OK, just increase the line counter
default { "Bad delimiter count in line {0}: '{1}'" -f ++$line, $_ }
}
Output:
Bad delimiter count in line 2: 'blah|using|four |delimiter |characters'
Bad delimiter count in line 4: 'blah|using|four |delimiter |characters'
Bad delimiter count in line 5: 'blah|using two |delimiters'
Regex details:
^ Assert position at the beginning of the string
(?: Match the regular expression below
[^|] Match any character that is NOT a “|”
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
\| Match the character “|” literally
){3} Exactly 3 times
[^|] Match any character that is NOT a “|”
* Between zero and unlimited times, as many times as possible, giving back as needed (greedy)
$ Assert position at the end of the string (or before the line break at the end of the string, if any)

How to compare two sequential strings in a file

I have a big file consists of "before" and "after" cases for every item as follows:
case1 (BEF) ACT
(AFT) BLK
case2 (BEF) ACT
(AFT) ACT
case3 (BEF) ACT
(AFT) CLC
...
I need to select all of the strings which have (BEF) ACT on the "first" string and (AFT) BLK on the "second" and place the result to a file.
The idea is to create a clause like
IF (stringX.LineNumber consists of "(BEF) ACT" AND stringX+1.LineNumber consists of (AFT) BLK)
{OutFile $stringX+$stringX+1}
Sorry for the syntax, I've just starting to work with PS :)
$logfile = 'c:\temp\file.txt'
$matchphrase = '\(BEF\) ACT'
$linenum=Get-Content $logfile | Select-String $matchphrase | ForEach-Object {$_.LineNumber+1}
$linenum
#I've worked out how to get a line number after the line with first required phrase
Create a new file with a result as follows:
string with "(BEF) ACT" following with a string with "(AFT) BLK"
Select-String -SimpleMatch -CaseSensitive '(BEF) ACT' c:\temp\file.txt -Context 0,1 |
ForEach-Object {
$lineAfter = $_.Context.PostContext[0]
if ($lineAfter.Contains('(AFT) BLK')) {
$_.Line, $lineAfter # output
}
} # | Set-Content ...
-SimpleMatch performs string-literal substring matching, which means you can pass the search string as-is, without needing to escape it.
However, if you needed to further constrain the search, such as to ensure that it only occurs at the end of a line ($), you would indeed need a regular expression with the (implied) -Pattern parameter: '\(BEF\) ACT$'
Also note PowerShell is generally case-insensitive by default, which is why switch -CaseSensitive is used.
Note how Select-String can accept file paths directly - no need for a preceding Get-Content call.
-Context 0,1 captures 0 lines before and 1 line after each match, and includes them in the [Microsoft.PowerShell.Commands.MatchInfo] instances that Select-String outputs.
Inside the ForEach-Object script block, $_.Context.PostContext[0] retrieves the line after the match and .Contains() performs a literal substring search in it.
Note that .Contains() is a method of the .NET System.String type, and such methods - unlike PowerShell - are case-sensitive by default, but you can use an optional parameter to change that.
If the substring is found on the subsequent line, both the line at hand and the subsequent one are output.
The above looks for all matching pairs in the input file; if you only wanted to find the first pair, append | Select-Object -First 2 to the Select-String call.
Another way of doing this is to read the $logFile in as a single string and use a RegEx match to get the parts you want:
$logFile = 'c:\temp\file.txt'
$outFile = 'c:\temp\file2.txt'
# read the content of the logfile as a single string
$content = Get-Content -Path $logFile -Raw
$regex = [regex] '(case\d+\s+\(BEF\)\s+ACT\s+\(AFT\)\s+BLK)'
$match = $regex.Match($content)
($output = while ($match.Success) {
$match.Value
$match = $match.NextMatch()
}) | Set-Content -Path $outFile -Force
When used the result is:
case1 (BEF) ACT
(AFT) BLK
case7 (BEF) ACT
(AFT) BLK
Regex details:
( Match the regular expression below and capture its match into backreference number 1
case Match the characters “case” literally
\d Match a single digit 0..9
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
\( Match the character “(” literally
BEF Match the characters “BEF” literally
\) Match the character “)” literally
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
ACT Match the characters “ACT” literally
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
\( Match the character “(” literally
AFT Match the characters “AFT” literally
\) Match the character “)” literally
\s Match a single character that is a “whitespace character” (spaces, tabs, line breaks, etc.)
+ Between one and unlimited times, as many times as possible, giving back as needed (greedy)
BLK Match the characters “BLK” literally
)
My other answer completes your own Select-String-based solution attempt. Select-String is versatile, but slow, though it is appropriate for processing files too large to fit into memory as a whole, given that it processes files line by line.
However, PowerShell offers a much faster line-by-line processing alternative: switch -File - see the solution below.
Theo's helpful answer, which reads the entire file into memory first, will probably perform best overall, depending on file size, but it comes at the cost of increased complexity, due to relying heavily on direct use of .NET functionality.
$(
$firstLine = ''
switch -CaseSensitive -Regex -File t.txt {
'\(BEF\) ACT' { $firstLine = $_; continue }
'\(AFT\) BLK' {
# Pair found, output it.
# If you don't want to look for further pairs,
# append `; break` inside the block.
if ($firstLine) { $firstLine, $_ }
# Look for further pairs.
$firstLine = ''; continue
}
default { $firstLine = '' }
}
) # | Set-Content ...
Note: The enclosing $(...) is only needed if you want to send the output directly to the pipeline to a cmdlet such as Set-Content; it is not needed for capturing the output in a variable: $pair = switch ...
-Regex interprets the branch conditionals as regular expressions.
$_ inside a branch's action script block ({ ... } refers to the line at hand.
The overall approach is:
$firstLine stores the 1st line of interest once found, and when the 2nd line's pattern is found and $firstLine is set (is nonempty), the pair is output.
The default handler resets $firstLine, to ensure that only two consecutive lines that contain the strings of interest are considered.