10
grep -A 2 -B 3 

prints 2 lines after the grep string and prints 3 lines before.

grep -C 3

prints 3 Lines before and 3 lines after

Unfortunately, the grep I'm using does not support these options. Are there any alternative commands or script available to simulate this? Using sed/awk/perl/shell scripts?

  • +1 I did not know there was a -C switch. – Lazer Apr 13 '11 at 01:42
  • 1
    Install GNU's grep. More generally, when a new Sun machine arrived here, the very first step in the setup was what somebody called GNU > /usr/local. The GNU programs have lots of very useful extensions, and are designed to avoid arbitrary restrictions (but you do pay dearly in size and sometimes performance). Many propietary systems have "unofficial" package repositories with GNU and other tools. The "partner" won't tell you about them, even when they are managed by the vendor... – vonbrand Jan 23 '13 at 14:36

6 Answers6

6

One moderately ugly way to do it is

grep -v pattern file >file.tmp; diff -c file.tmp file

or replace -c with -C NUM for NUM lines of context. It'll produce extra output, though. (If your diff supports -u/-U NUM, it'll be cleaner.)

If your diff doesn't have -c/-C/-u, there are still ways to do it, but they're pretty ugly. On the other hand, a system whose diff doesn't even support -c probably doesn't have Perl either.

geekosaur
  • 32,047
5

This simple perl script emulates grep -A to some extent

#!/usr/bin/perl

$pattern=shift; #patthern to search
$lines=shift; # number of lines to print

$n = 0;
while (<>) {
  $n = $lines if /$pattern/; # reset counting
  if ($n) { print; $n-- } # print if within
  $n = 0 if eof; # don't leak across file boundaries
}

Note that you may add a usage statement, to make script readable and usable ;)

USAGE:    $./grep-A.pl <pattern> <numLines> <filename> 
Kevin
  • 40,767
5

ack requires only Perl, and includes -A, -B, and -C options that work like grep's. It uses Perl's regex syntax instead of grep's, and the way it selects files to search is quite different. You might want to try the -f option when using it (which prints out the files it will search without actually searching anything).

It can be installed as a single script that requires no non-core modules. Just drop it into your ~/bin directory (or anywhere else on your PATH that you have write access to) and make sure it's chmod'd executable.

cjm
  • 27,160
  • Its production box and Unfortunately I dont have enough privilege to install anything, and I cant risk it, though, thanks for this tip I will install it and try on my home laptop – Prashant Bhate Apr 12 '11 at 20:49
  • @Prashant, you don't need root to install ack for your own use. – cjm Apr 12 '11 at 21:10
  • Yes but still I cant use it there, though its sure that this script will stay forever in my ~/bin :) – Prashant Bhate Apr 12 '11 at 22:11
  • @Prashant: Why can't you use it? It's just a perl script. – intuited Apr 13 '11 at 09:21
  • 1
    Its PRODUCTION box, need to take special permissions approvals bla bla bla... todo any thing on it. and anything goes wrong there on comes on my head ;) and Its not worth it :) – Prashant Bhate Apr 13 '11 at 10:24
3

You can just install GNU grep or Ack (written in Perl, understands many of GNU grep's options and more).

If you prefer to stick to standard tools plus a bit of scripting, here's an awk script that emulates the behavior of GNU grep's -A and -B options. Minimally tested.

#!/bin/sh
# grep-ac: a grep-like awk script
# Arguments: pattern = awk regexp to search for
#            before = number of lines to print before a match
#            after = number of lines to print after a match
{ "exec" "awk" "-f" "$0" "$@"; }
# The array h contains the history of lines that haven't been printed
# but are eligible for being "before" lines.
# The variable until contains the number of the last "after" line to print.
match($0, pattern) {   # the current line matches
    for (i in h) {
        print h[i];    # print each remaining before line
        delete h[i];   # delete each line as it's printed
    }
    until=NR+after;    # record the last after line to print
}
{
    if (NR<=until) print $0;    # from a match to its last after line: print
    else h[NR]=$0;              # after that: save in history
    delete h[NR-before];        # remove line too old to be a before line
}
END {exit !until}               # exit status: 0 if there was a match, else 1

Run it as grep-ac -vpattern=PATTERN -vbefore=NBEFORE -vafter=NAFTER where PATTERN is the pattern to search for (an extended regular expression with a few awk additions), and NBEFORE and NAFTER are the numbers of lines to print before and after a match respectively (defaulting to 0). Example:

<input_file grep-ac -vbefore=2 -vpattern='foo *bar'
  • Any solution that stores data in array is out of question ... as I mentioned earlier , filesize is quite huge and it might over flow. Also awk on this system does not allow file size more than 3000 bytes. – Prashant Bhate Apr 12 '11 at 22:19
  • 2
    @Prashant: I don't understand your objections. This script deletes lines once they're ineligible to be before-lines. It doesn't use more memory than is inherently necessary given the requirements, except that awk may have a higher overhead than a special-purpose program (but less than Perl, which you're also considering). The total size of the file is completely irrelevant. – Gilles 'SO- stop being evil' Apr 12 '11 at 22:28
  • 2
    { "exec" "awk" "-f" "$0" "$@"; }: very nifty way to get around the limitations in shebang-line parsing. – dubiousjim Oct 18 '12 at 21:50
2

It turns out that it's quite tricky to emulate -B, because of the issues that crop up when you have matching lines following each other directly. This pretty much disallows using any sort of single-pass-through file scanning.

I realized this while playing around with the following approximation:

perl -pe 'if(/search_term/) {print foreach @A; print ">"; $B=4}; shift @A if push(@A, $_)>7; $_ = "" unless ($B-- > 0);' target_file

This will work roughly correctly as grep -A7 -B3 would, with the caveat described in the first paragraph.

An alternative (also single-file) solution to this issue is to use perl to feed sed a command string:

sed -n `perl -pe '$_=(/search_term/?sprintf("%d,%dp;", $.-3,$.+4):"")' file` file
user455
  • 1,260
  • pretty lenghty oneliner, but, this file is very huge, so pushing lines into array in this case is a bad Idea, isnt it ? – Prashant Bhate Apr 12 '11 at 20:51
  • The shift @A if push(@A,$_)>7; bit only keeps an array of maximum size 7 around. (that's your -A parameter). The second option keeps an incredibly small file around (just run the perl without the sed outer layer to see what is generated there), but it does read the file twice. – user455 Apr 12 '11 at 20:54
0

Using sed you can first get the line numbers of matching lines, decrement & increment a given line number in a while loop and then use sed -n "n1,n2p" to print lines of leading (n1) and trailing (n2) context (similar to the sed alternative suggested by user455). Many read processes may lead to a performance hit though.

ed can directly reference the previous and following lines of a matched line, but fails if the specified line range does not exist; for example, matching line is line number 2, but 5 pre-match lines should be printed. Using ed it is therefore necessary to add an appropriate number of (empty) lines at the beginning and the end. (For huge files ed may not be the right tool though, see: bfs - big file scanner).

# sample code to match lines with number 5 plus previous & following line
# (using Bash)
printf '%s\n' {1..20} > num.txt

# sed
sed -n '/5/=' num.txt | while read num; do
   n1=$((num - 1))
   n2=$((num + 1))
   [[ $n1 -lt 1 ]] && n1=1
   sed -n "${n1},${n2}p" num.txt
   echo --
done | sed -e '${/^--$/d;}'

# ed
cat <<-'EOF' | ed -s num.txt | sed -e $'N;N;a\\\n--' | sed -e '${/^--$/d;}'
H
0i
beginning: added line one
.
$a
end: added line one
.
,g/5/km\
'm-1,'m+1p
q
EOF
larz
  • 31