0

I currently ran into a problem that needs some help solving. Even pointing in the correct direction would help.

I have a file with lots of lines and want to extract only specific 'group' of lines if they are following a pattern. (Line has to start with A, next line has to start with B, next line has to start with C)

For example: Pattern: Starting with A, B, C in this order.

Input:

A1
B1
C1
D1
A2
B2
D2
A3
D3
A4
B4
C4
A5
B5
D5

Output:

A1
B1
C1
A4
B4
C4
4

Another solution with awk:

awk 'p2~/^A/ && p1~/^B/ && /^C/{print p2 RS p1 RS $0} {p2=p1; p1=$0}'

With perl and slurping entire input as single string:

perl -0777 -ne 'print /^A.*\nB.*\nC.*\n/mg'

With ripgrep which supports a handy multiline matching option -U

rg -oUN '^A.*\nB.*\nC.*'

Here, -o option gets only matching portion and -N option is to prevent line numbers in output

2

If you would also like a Sed solution, this is ugly but should work.

sed -n '
  /^A/{
    N
    /\nB/!D
    N
    /\nC/!{
      s/\n//
      D
    }
    p
  }
' file

-n tells sed not to print anything unless it reaches the p command.

If you understand the first block, you can understand the rest:

  • /^A/ If pattern space starts with A,
    • N Append next line to pattern space.
    • /\nB/!D If pattern space does not have a newline followed by B, delete all up to the first newline and start again with the resulting pattern space and without reading any input.

One liner: sed -n '/^A/{N;/\nB/!D;N;/\nC/!{s/\n//;D};p}' file

1
  • I like this one the most since it is easily adjustable, readable and understandable. Thank you. Sep 17 '20 at 6:52
1

The following awk program should work:

awk 's==2{if (/^C/) {s=0; p=p ORS $0; print p} else {s=0}}\
     s==1{if (/^B/) {s=2; p=p ORS $0} else {s=0}}\
     s==0{if (/^A/) {s=1; p=$0}}' input.txt 

This will keep an internal "status" flag s to see at which point in the sequence we are (0: start not found, 1: A found, 2: A and B found in sequence), and accumulating the text in a buffer p.

  • If A and B was found, and the current line starts with C, we add the current line to the buffer and print it. The status is reset to 0
  • If A was found, and the current line starts with B, we add the current line to the buffer and set the status to 2 (=A and B found)
  • If the start was not yet found, and the current line starts with A, we add this to the buffer and set the status to 1.
1
  • It works thank you. sed would be easier for me to process the rest but I can work with this. Sep 16 '20 at 12:16
1

This relies on a multi-line pattern space and discards the top-most line as soon a match is not found and the search is then started over. Lines are added after each match. A group is fully matched when all pieces are matched in turn:

sed '/^A/!D; /\n/!N; /\nB/!D; N; /\nC/!D'

The solution can be generalised to match an arbitrary number of lines, and using a bit more care to isolate search terms. The script below will match a 4-line group (e.g. A|B|A|B), simply substitute successive lines for each "X":

sed '
    # If X does not begin pattern space, delete first line,
    # return to script start, read a line if none remain
    /^X/!D
    # Append next line if pattern space holds 1 line
    /\n/!N

    # If X does not begin line 2+ of pattern space, delete first line,
    # return to script start, read a line if none remain
    /\nX/!D
    # Append next line if pattern space holds < 3 lines
    /\n.*\n/!N

    # If X does not begin line 3+ of pattern space, delete first line,
    # return to script start, read a line if none remain
    /\n.*\nX/!D
    # Append next line if pattern space holds < 4 lines
    /\n.*\n.*\n/!N

    # If X does not begin line 4+ of pattern space, delete first line,
    # return to script start, read a line if none remain
    /\n.*\n.*\nX/!D

    # After here, output pattern space then discard and start again
'
0
0

you can setup a state machine in sed to advance to the next state only if the previous state would lead you A->B->C

Otherwise, chop the head and restart.

$ sed -e '$d;/\n/d
    /^A/N;/\nB/!D
    $!N;/\nC/!D
' file
A1
B1
C1
A4
B4
C4

if you have gnu grep compiled with the PCRE option, then the following grep code Is another way.

$ grep -zoP '(?m)^A.*\nB.*\nC.*\n' file | tr -d '\0'
2
  • Thanks for pointing that out. Pugged the leak now.it would not have printed A1|B2|C1 but B1|B2|C1 Sep 16 '20 at 13:55
  • You are welcome! But I'm afraid that Sed still fails for A4|B4|A4|B5|C5, printing nothing.
    – Quasímodo
    Sep 16 '20 at 16:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.