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

5 Answers 5

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, 2020 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, 2020 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, 2020 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, 2020 at 16:13

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .