Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

Using OPEN STEP 4.2 OS... I am currently using the following sed Command:

sed -n '1,/141.299.99.1/p' TESTFILE | tail -3

This command will find one instance in a file with the ip of 141.299.99.1 and also include 3 lines before it which is all good, with the exception that I would also like to find all the instances of the IP and the 3 lines before it and not just the first.

share|improve this question
1  
Please always include your OS. Solutions very often depend on the Operating System being used. Are you using Unix, Linux, BSD, OSX, something else? Which version? –  terdon Jul 23 at 23:59
    
GREAT POINT! Using Open Step version 4.2 is quite old and the included shells don't include many of the features mentioned in the answers below. –  Dale Jul 24 at 17:27
    
Out of curiosity - what is an OPEN STEP 4.2 system and what is it used for today? –  Thorbjørn Ravn Andersen Jul 24 at 21:14
    
(and if Perl is available you can really do a lot of nice things just with that) –  Thorbjørn Ravn Andersen Jul 24 at 21:18
    
@ThorbjørnRavnAndersen Maybe it's this: en.wikipedia.org/wiki/OpenStep –  Barmar Jul 30 at 20:53

9 Answers 9

up vote 4 down vote accepted

Here's an attempt to emulate grep -B3 using a sed moving window, based on this GNU sed example (but hopefully POSIX-compliant - with acknowledgement to @StéphaneChazelas):

sed -e '1h;2,4{;H;g;}' -e '1,3d' -e '/141\.299\.99\.1/P' -e '$!N;D' file

The first two expressions prime a multi-line pattern buffer and allow it to handle the edge case in which there are fewer than 3 lines of preceding context before the first match. The middle (regex match) expression prints a line off the top of the window until the desired match text has rippled up through the pattern buffer. The final $!N;D scrolls the window by one line except when it reaches the end of input.

share|improve this answer
    
-e is not GNU specific. To be POSIX/portable, you do need it as there can't be anything after } (and you need a ; before it). –  Stéphane Chazelas Jul 24 at 5:58
    
Thanks @StéphaneChazelas - so are you saying that to be POSIX/portable, the first group needs to be split/modified as -e '1h;2,4{H;g;}' -e '1,3d'? I don't have a non-GNU system to test on (and the GNU sed --posix switch doesn't seem to care). –  steeldriver Jul 24 at 12:12
1  
Yes, on Linux, you can test a different implementation with the sed from the heirloom toolchest which is a descendant of the traditional Unix sed. The POSIX/Unix spec for sed is at pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html –  Stéphane Chazelas Jul 24 at 12:18
    
I am getting event not found on either of these: N;D': Event not found. Am I missing syntax somewhere? Thanks!! –  Dale Jul 24 at 15:45
    
Sorry I just realized my most recent edit omitted a closing single quote after the first -e expression. I have corrected it now - can you try again with the above expression please? –  steeldriver Jul 24 at 17:06

grep will do a better job of this:

grep -B 3 141.299.99.1 TESTFILE

The -B 3 means to print the three lines before each match. This will print -- between each group of lines. To disable that, use --no-group-separator as well.

The -B option is supported by GNU grep and most BSD versions as well (OSX, FreeBSD, OpenBSD, NetBSD), but it is technically not a standard option.

share|improve this answer
1  
Michael Homer - Thank you. I don't have the - B option. Any other ideas? –  Dale Jul 23 at 22:45
    
@Dale Can you install GNU grep? That will give you the option. –  Barmar Jul 30 at 20:54

With sed you can do a sliding window.

sed '1N;$!N;/141.299.99.1/P;D'

That does it. But beware - bash's insane behavior of expanding ! even when quoted!!! into the command string from your command history might make it go a little crazy. Prefix the command with set +H;if you find this is the case. To then re-enable it (but why???) do set -H afterward.

That, of course, would only apply if you were using bash - though I don't believe you are. I'm fairly certain you're working with csh - (which happens to be the shell whose insane behavior bash emulates with the history expansion, but maybe not to the extremes the c shell took it). So probably a \! should work. I hope.

It's all portable code: POSIX describes its three operators thus: (though it's worth noting that I've only confirmed this description existed as early as 2001)

[2addr]N Append the next line of input, less its terminating \newline, to the pattern space, using an embedded \newline to separate the appended material from the original material. Note that the current line number changes.

[2addr]P Write the pattern space, up to the first \newline, to standard output.

[2addr]D Delete the initial segment of the pattern space through the first \newline and start the next cycle.

So on the first line you add an extra line to pattern space, so it looks like this:

^line 1s contents\nline 2s contents$

Then on the first line and every line thereafter - excepting the very last - you add another line to pattern space. So it looks like this:

^line 1\nline 2\nline 3$

If your ip address is found within you Print up to the first newline, so just line 1 here. At the end of every cycle you Delete same and start over with what remains. So the next cycle looks like:

^line 2\nline 3\nline 4$

...and so on. If your ip is to be found on any one of those three the oldest will print out - everytime. So you're always only three lines ahead.

Here's a quick example. I'll get a three line buffer printed for every number ending in zero:

seq 10 52 | sed '1N;$!N;/0\(\n\|$\)/P;D'

10
18
19
20
28
29
30
38
39
40
48
49
50

That one's a little more complicated than your case because I had to alternate from either 0\n newline or 0$ end of pattern space to more closely resemble your problem - but they are subtly different in that this requires an anchor - which can be a little difficult to do since pattern-space constantly shifts.

I used the odd cases of 10 and 52 to show that as long as the anchor is flexible then so is the output. Fully portably I can achieve the same results by instead counting on the algorithm and do:

seq 10 52 | sed '1N;$!N;/[90]\n/P;D'

And widen the search while restricting my window - from 0 to 9 and 0 and from 3 lines to two.

Anyway, you get the idea.

share|improve this answer
    
Thanks for all your hard work. Sorry, where would I put the file name I would like it to search through? –  Dale Jul 24 at 19:45
    
@Dale - my bad. sed '...' $filename. By the way - I left in the periods from your own search string, but those aren't actually periods in a pattern - those represent any single character. You should probably do oct\.oct\.oct\.oct to escape them so they only match periods. –  mikeserv Jul 24 at 19:48
    
I tried to cat with it and different <> symbols and I get event not found which I get with other solutions here so I wonder if my OS isn't compatiable with these solutions. –  Dale Jul 24 at 19:51
    
trying now.. Thanks! –  Dale Jul 24 at 19:52
    
now results with ->N;/141.299.99.1/P;D': Event not found. –  Dale Jul 24 at 19:56

If your system doesn't support grep context, you can try ack-grep instead:

ack -B 3 141.299.99.1 file

ack is a tool like grep, optimized for programmers.

share|improve this answer
    
I like the compactness of the command but my system doesn't support ack in looking in the man pages. Great idea and thank you so much for your time!!! Dale –  Dale Jul 24 at 15:47
    
@Dale: Suprising! What is your OS? If you have perl, you can use ack. –  cuonglm Jul 24 at 15:49

Since you mention that you don't have the -B option to grep, you can use Perl (for example) to make a sliding a window of 4 lines:

perl -ne '
    push @window,$_;
    shift @window if @window > 4;
    print @window if /141\.299\.99\.1/
' your_file

Ramesh's answer does a similar thing with awk.

share|improve this answer
    
I am not sure if my version of Perl supports this but I will give it a try. Thank you so much for taking the time to answer my question - very grateful! –  Dale Jul 24 at 14:52
    
@Dale You're very welcome. I doubt that this code makes use of any cutting-edge Perl features. –  Joseph R. Jul 24 at 15:10

When available you can use pcregrep:

pcregrep -M '.*\n.*\n.*\n141.299.99.1' file
share|improve this answer
    
Checking into if I have PCREGREP. I like the compactness of the command. Very grateful for your time and efforts. Thank you!!! –  Dale Jul 24 at 15:01

You can implement the same basic approach as the other non-grep answers in the shell itself (this assumes a relatively recent shell that supports =~):

while IFS= read -r line; do 
    [[ $line =~ 141.299.99.1 ]] && printf "%s\n%s\n%s\n%s\n" $a $b $c $line;
    a=$b; b=$c; c=$line; 
done < file 

Alternatively, you could slurp the whole file into an array :

perl -e '@F=<>; 
        for($i=0;$i<=$#F;$i++){
          print $F[$i-3],$F[$i-2],$F[$i-1],$F[$i] if $F[$i]=~/141.299.99.1/
        }' file 
share|improve this answer
    
My shell is very old - Steve Jobs Open Step. Great idea though and thank you for your time!!! Dale –  Dale Jul 24 at 15:50
    
@Dale the perl approach will work just about anywhere. Please tell us your operating system (add it to your question) that way we can suggest things that will work for you. –  terdon Jul 24 at 15:55
    
If I copy your Perl and put it into NotePad and put it on one line it works! Question - if I wanted lets say 10 lines before the match pattern where would I change the 3 to 10? Thanks! –  Dale Jul 24 at 20:21
    
I see that I can add more lines back by adding more $F[$i-X], statements. Thanks! –  Dale Jul 24 at 20:59
awk '/141.299.99.1/{for(i=1;i<=x;)print a[i++];print} {for(i=1;i<x;i++)
     a[i]=a[i+1];a[x]=$0;}'  x=3 filename

In this awk solution, an array is used which will always contain 3 lines before the current pattern. Hence, when the pattern is matched, the array contents along with the current pattern is printed.

Testing

-bash-3.2$ cat filename
10.0.0.1
10.0.0.2
10.0.0.3
10.0.0.4
141.299.99.1
10.0.0.5
10.0.0.6
10.0.0.7
10.0.0.8
10.0.0.9
10.0.0.10
141.299.99.1
10.0.0.11
10.0.0.12
10.0.0.13
10.0.0.14
10.0.0.15
10.0.0.16
141.299.99.1
10.0.0.17
10.0.0.18
10.0.0.19

After I execute the command, the output is,

10.0.0.2
10.0.0.3
10.0.0.4
141.299.99.1
10.0.0.8
10.0.0.9
10.0.0.10
141.299.99.1
10.0.0.14
10.0.0.15
10.0.0.16
141.299.99.1
share|improve this answer
    
so detailed - thank you very much. I'll give it a try. Very grateful for your time!! Dale –  Dale Jul 24 at 15:00
    
I have a test file and your solution works! The issue though is when I run it on my large production file it comes back with Too Long Record Number so the output is not able to work with the command. My original command at the top of this page works but only finds one instance. I appreciate your help. Is there anything I can do with my original command to make it find more than one instatnce? –  Dale Jul 24 at 20:14

In most of these, /141.299.99.1/ will also match (e.g.) 141a299q99+1 or 141029969951 because . in a regular expression can represent any character.

Using /141[.]299[.]99[.]1/ is safer, and you can add additional context at the beginning and end of the whole regexp to make sure it doesn't match 3141., .12, .104 , etc.

share|improve this answer
1  
This is a good point - and one I also considered. Still, I used the string provided by the asker as a known working match - and notified him personally of the same when provided opportunity. Anyway - not all of these - steeldriver's answer has quoted the char match from the start. –  mikeserv Jul 24 at 22:05

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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