Get grep context with multiline regex

Viewed 163

Do grep's -Pz and -C options work together? I'm trying to match an adjacent-lines phrase and print it's context. The extended regex and context options work separately, but not together like this (prints the whole file):

grep -C 2 -Pz ".*word.*\n.*phrase.*" file.txt

content of file.txt:

line 1
line 2
line 3
line 4
line 5
word ...other text
phrase ...yet another text
line 6
line 7
line 8
line 9
line 10

expected result:

line 4
line 5
word ...other text
phrase ...yet another text
line 6
line 7
2 Answers

No. You cannot. It is not allowed. -Pz and -C do not like each other. Fear not, There is a way to do what you want to do:

grep -Pzo ".*\n.*\n.*.*word.*\n.*phrase.*\n.*\n.*" file.txt

Or you could parameterize it

BEFORE=2
AFTER=2
grep -Pzo "(.*\n){$BEFORE}.*word.*\n.*phrase.*(\n.*){$AFTER}" file.txt
  1. You use -Pzo do only print out lines that match your specified pattern.
  2. Include some number of .*\n.*'s padding your pattern string.

You may find this bash function useful:

function pad_grep()(
        usage() { echo "Usage: $0 [-ABC] [EXPR] [FILE]" 1>&2; exit 1; }

        A=0
        B=0
        while getopts "A:B:C:" flag; do
                case "$flag" in
                        A) A=$OPTARG;;
                        B) B=$OPTARG;;
                        C) A=$OPTARG;B=$OPTARG;;
                        *) usage;;
                esac
        done
        EXPR=${@:$OPTIND:1}
        FILE=${@:$OPTIND+1:1}

        # Error checking
        [ ${#EXPR} -eq 0 ] && usage
        [[ ${#FILE} -ne 0 && ! -f ${FILE} ]] && usage

        grep -Pzo "(.*\n){$B}${EXPR}(\n.*){$A}" $FILE
)

# Do it yourself
grep -Pzo ".*\n.*\n.*\n.*.*word.*\n.*phrase.*\n.*\n.*" file.txt

# Use the function
pad_grep -B 3 -A 2 '.*word.*\n.*phrase.*' file.txt
pad_grep -C 2 '.*word.*\n.*phrase.*' file.txt

Likely your issue is the z flag to GNU grep which then changes of the definition of a line to terminated by a \0

Easy to demo. Given:

$ echo "$txt"
line 1
line 2
line 3
line 4
line 5
word ...other text
phrase ...yet another text
line 6
line 7
line 8
line 9
line 10

You can do:

$ echo "$txt"  | ggrep --context=2  -Pz "word|phrase"
# prints all the lines

Or:

$ echo "$txt"  | ggrep --context=2  -P "word|phrase"
line 4
line 5
word ...other text
phrase ...yet another text
line 6
line 7

Which you can demonstrate will work with z by actually have NUL terminations for the lines:

$ echo "$txt" | tr '\n' '\0' | ggrep --context=2  -Pz "word|phrase" | tr '\0' '\n'
line 4
line 5
word ...other text
phrase ...yet another text
line 6
line 7

For perl regex's and logic of before and after and multilines, you might as well just use Perl!

Given:

$ cat file
line 1
line 2
line 3
line 4
line 5
word ...other text
betweener 1, line 7
betweener 2, line 8
phrase ...yet another text
line 10
line 11
line 12
line 13
line 14

You can do:

# $b=2 is equivalent to grep -B 2, or lines before
# $a=2 is equivalent to grep -A 2, or lines after
$ perl -lne 'BEGIN{$b=2; $a=2;}
             print join("\n", @a) if (/word/);
             print if (/word/../phrase/) || ($c && $c--);
             $c=$b if (/phrase/);
             shift @a if push(@a, $_)>$a;' file

Or, you can use POSIX or GNU awk as well:

$ awk 'BEGIN{b=2; a=2}
   /word/ { for (i=FNR-b;i<=FNR-1;i++) 
                 print arr[i]   # print the lines before the first match
            f=1}                # flag we are in the match
    f || (c && c--)             # print either if in the match or tail context
    /phrase/ {f=0; c=a}          # end match, start tail
    {for (ln in arr) 
         if (ln<FNR-b) delete arr[ln] # rolling line buffer
    arr[FNR]=$0}                # save current line
' file

Either prints:

line 4
line 5
word ...other text
betweener 1, line 7
betweener 2, line 8
phrase ...yet another text
line 10
line 11

That works even with no 'betweener' lines.

Related