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

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)

Related

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.

Remove a line using PowerShell

I would like to remove a empty line in using powershell.
Here is my Text
Line 1
Line 2
<Empty line>
<Empty line>
Line 3
Line 4
I have tried using -replace ("`n", "") but it does not work.
Please help!
$text = """
Line 1
Line 2
Line 3
Line 4
"""
$text -replace "(?m)^\s*`r`n",''
# Or
$text -replace "(?m)^\s*`n",''
regex explained:
(?m) set the multiline flag so ^ matches the beginning of each line and not just the begining of the whole string.
^ matches the beginning of a line
\s* matches any number of white spaces (could also be 0 white spaces)
`r`n matches the end of a line.
Why `r`n or just `n (depending on the machine or file):
\`r = CR (Carriage Return) → Used as a new line character in Mac OS before X
\`n = LF (Line Feed) → Used as a new line character in Unix/Mac OS X
\`r\`n = CR + LF → Used as a new line character in Windows
Or simply read the file as string array and use a Where-Object to let pass only the lines that contain at least one non-whitespace character.
$text = Get-Content -Path 'D:\Thefile.log' |
Where-Object { $_ -match '\S' }
Now you can save this string array in $text back to file, join it with [environment]::NewLine to become a single string again, or...

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.

Find lines between a pattern, and append 1st line to lines

I have the following case I'm trying to script in Powershell. I have done this exercise using Sed on a bash terminal, but having trouble writing in Powershell. Any help would be greatly appreciated.
(sed -r -e '/^N/h;/^[N-]/d;G;s/(.*)\n(.*)/\2 \1/' <file>, with a file format without < and > chars. surrounding the first letter on each line)
The start pattern always start with a <N> (only 1 instance per block), lines between start with a <J>, and the end pattern is always --
--------------
<N>ABC123
<J>SomethingHere1
<J>SomethingHere2
<J>SomethingHere3
-------------- <-- end of section
I'm trying to take the first line in each section <N> and copy it AFTER each <J> in the same section. For example:
<J>SomethingHere1 <N>ABC123
<J>SomethingHere2 <N>ABC123
<J>SomethingHere3 <N>ABC123
The number of <J> lines per section can vary (0-N). In a case with no <J>, nothing needs to be done.
Powershell version:5.1.16299.611
The following, pipeline-based solution isn't fast, but conceptually straightforward:
Get-Content file.txt | ForEach-Object {
if ($_ -match '^-+$') { $newSect = $true }
elseif ($newSect) { $firstSectionLine = $_; $newSect = $False }
else { "{0}`t{1}" -f $_, $firstSectionLine }
}
It reads and processes lines one by one (with the line at hand reflected in automatic variable $_.
It uses a regex (^-+) with the -match operator to identify section dividers; if found, flag $newSect is set to signal that the next line is the section's first data line.
If the first data line is hit, it is cached in variable $firstSectionLine, and the $newSect flag is reset.
All other lines are by definition the lines to which the first data line is to be appended, which is done via the -f string-formatting operator, using a tab char. (`t) as the separator.
Here's a faster PSv4+ solution that is more complex, however, and it reads the entire input file into memory up front:
((Get-Content -Raw file.txt) -split '(?m)^-+(?:\r?\n)?' -ne '').ForEach({
$firstLine, $otherLines = $_ -split '\r?\n' -ne ''
foreach ($otherLine in $otherLines) { "{0}`t{1}" -f $otherLine, $firstLine }
})
Get-Content -Raw reads in the input file in full, as a single string.
It uses the -split operator to split the input file into sections, and then processes each section.
Regex '(?m)^-+(?:\r?\n)?' matches a section divider line, optionally followed by a newline.
(?m) is the multiline option, which makes ^ and $ match the start and end of each line, respectively:
\r?\n matches a newline, either in CRLF (\r\n) or LF-only (\n) form.
(?:...) is a non-capturing group; making it non-capturing prevents what it matches from being included in the elements returned by -split.
-ne '' filters out resulting empty elements.
-split '\r?\n' splits each section into individual lines.
If performance is still a concern, you could speed up reading the file with [IO.File]::ReadAllText("$PWD/file.txt").

powershell replace text between 2 lines

I have a text file and want to replace the text between 2 lines. This is working OK until there is no text between the two lines.
my code:
$File = "D:\test.txt"
$NewLine = "newline with some text"
$text = Get-Content "D:\test.txt" -raw
$text -replace ('(?m)(.*)^Line 3[\r\n]+Line 4([\r\n])', $NewLine) |
Out-File $File -Force
Text files:
Text file that does work:
Line 1
Line 2
Line 3
Line 4
Line 5
Line 6
Text file that does NOT work:
Line 1
Line 2
Line 3
Some text here
Line 4
Line 5
Line 6
What I am doing wrong?
The issue you have is that your Regular Expression (RegEx for short) does not allow for anything between Line 3 and Line 4. Here's what you're using (cleaned up a tiny bit):
(?m)^Line 3[\r\n]+Line 4[\r\n]
Let us break that down just a little bit. (?m) puts the RegEx engine into multi-line mode, so it will allow you to use the carat ^ to indicate the beginning of a line, instead of just the beginning of the string. There's other things it does, but that's what you are using it for. I discarded the (.*), because it is pointless. So you have a carat indicating the beginning of a line, followed by the text Line 3, and then [\r\n]+ which will find the end of the line. Then it finds Line 4, immediately followed by the end of that line. This is why it fails when there is text, you didn't allow for any additional text. To do that you can use .*?. What that means is this: The dot indicates any character, doesn't matter if it's letter, number, symbol, or even a character that doesn't register on the screen. The asterisk * indicates that there needs to be zero or more of that, so zero or more of any character. The question mark tells it to look for zero or more, but to match as few characters as possible before moving on in the pattern, so it will only match things until it can move on to Line 4[\r\n]. The functional pattern would be:
(?m)^Line 3[\r\n]+.*?[\r\n]Line 4[\r\n]
Corrected your regex here to capture only Line 3 and Line 4 with the text inbetween.
$File = 'D:\test.txt'
$NewLine = 'newline with some text'
$text = Get-Content -Path 'D:\test.txt' -Raw
$Pattern = '(?m)^Line\s3[\r\n]+.*?Line\s4'
$text -replace $Pattern,$NewLine |
Out-File -FilePath $File -Force