7

Consider the following text file

one 1
two 2
three 3
four 4
five 5
six 6
seven 7
eight 8

I would like to access to the second line after the one which matched four. This would be the line

six 6

The resulting line (so the one above) would then be piped for further processing (say, a | cut -d' ' -f2).

Is there a way to do this in bash and other typical utilities? (otherwise I will script it in Python)

EDIT: in my specific case the occurrence of four (to take that example) is guaranteed unique. But the answers show interesting extended cases when it is not.

3
  • 3
    Will there always only be one occurrence of what you are trying to match? Will it always be two lines after? Mar 16, 2019 at 22:36
  • 3
    Use awk? But if you're more familiar with Python that would be quicker.
    – MZB
    Mar 16, 2019 at 23:25
  • @NasirRiley: good point - I edited my question (four will be unique)
    – WoJ
    Mar 18, 2019 at 8:42

6 Answers 6

16

There is nothing wrong with the previous two answers, but I thought I would make you aware that finding the third line after a pattern can be done in a single sed call:

sed -n "/four/ { n; n; p }" SourceData.txt

Because a single program does the work, this is more efficient than running multiple filters. The above command outputs the third line after every instance of "four", except where this occurs again in one of the two lines following a match (the other solutions don't handle this case in the expected manner either); also, no output is generated if the pattern is in the last or second-last line of the file, which may or may not be what you want.

To match the first instance only:

sed -n "/four/ { n; n; p; q }" SourceData.txt

(Note that this answer is as efficient as possible, by ending the scan as soon as the match is found.)

I add this solution because it is worth getting to know sed and, despite its rather off-putting syntax (regular expressions are bad enough!), it can often be extremely useful. This tutorial is a good introduction.

3
  • "the other solutions don't handle this case in the expected manner either" – No longer true. :) Mar 17, 2019 at 2:08
  • @KamilMaciorowski - I didn't think of awk: obviously another tool worth learning in detail. And there's a Grymoire tutorial, too!
    – AFH
    Mar 17, 2019 at 14:32
  • @KamilMaciorowski - I've just seen the edit. I thought of adding a note about the efficiency of adding q, but at the time I didn't know whether it answered the problem, so I didn't elaborate. The other answers (especially your own) should stand, since others with related problems may find them better solutions for their particular circumstances.
    – AFH
    Mar 18, 2019 at 10:48
10

Note: this answer was originally written before the OP clearly stated the pattern appears just once. It is designed not to miss any occurrence (unless near the end, so there's no "n-th line after") and I'm going to leave it this way. If you're sure there's only one occurrence or if you want only the first one to be found, you may consider some other solution that stops immediately and doesn't parse the whole input stream/file in vain.


This solution prints the current line iff there was a match two lines ago. It is slightly different from few other answers because it won't miss another match even if it occurs soon after the previous match.

awk -v delay=2 '{for (i=delay; i>=0; i--) t[i]=t[i-1]} /four/ {t[0]="m"} {if (t[delay]) print}'

Whenever there's a match, the information is stored in t[0]. With each line the t array is shifted (including shifting t[-1] to t[0] to reset the value of t[0]). The line is printed iff the array indicates there was a match two lines ago.

You can easily set a different delay (e.g. delay=7) or use another pattern (e.g. /sda[[:digit:]]/)

2
  • This is the best solution, as it handles all occurrences of the pattern. However, it is quite long, might as well write a Python solution :D
    – justhalf
    Mar 17, 2019 at 12:25
  • 2
    @justhalf One aspect though: awk is a standard POSIX tool, python is not. Mar 17, 2019 at 14:10
8

You can use this expression (input.txt):

grep "four" -A 2 input.txt | tail -n 1

Output is:

six 6

The grep option "-A 2" states that two lines after the matched line are outputted.
And the tail option "-n 1" states that only the last 1 lines of this result are returned.

1
  • 4
    Note that this only works if there's just a single match, or you're only interested in the last match.
    – Barmar
    Mar 17, 2019 at 7:53
5

Looks like a good use case for ex, the POSIX-specified scriptable file editor.

Unlike sed and awk, ex is actually designed for file editing, not stream editing, and is capable of going backwards and forwards in a file. It's actually the non-visual form of the vi editor.

But the important aspect here is that ex is capable of chaining addresses. So referring to the line that's two lines after a particular text pattern is trivial.

Here is a command which prints all the lines that come two lines after lines containing four:

printf '%s\n' 'g/four/+2p' | ex file.txt

I've written a lot of answers using ex on the Unix & Linux Stack Exchange; this one in particular has some additional explanations that may help.

4

For multiple occurences, and assumming that no lines start with --:

( grep -A 2  pattern data.txt; echo '--' ) | grep -E -B1 '^--' | grep -Ev '^--'

In slo-mo:

  • ( grep -A 2 pattern data.txt; echo '--' ) prints the pattern and the next two lines, and inserts a -- line between the groups. echo '--' makes sure that the last group is also followed by --.
  • grep -E -B1 '^--' print the separators and the lines just before (whoch are the ones we are looking for)
  • grep -Ev '^--' drops the separators, leaving only the lines we are looking for.
3
  • if you use the -A, -B or -C option then grep automatically use -- to separate matches, no need to print it yourself
    – phuclv
    Mar 17, 2019 at 0:47
  • Doesn't add a '--' after the last., and it there is none, the rest of the pipeline won't get the last target (unless I add a tail -1 to every stage).
    – xenoid
    Mar 17, 2019 at 10:16
  • 2
    I suggest searching for '^--$' as a way to reduce your failure mode still further. Mar 17, 2019 at 18:28
0

You already got very good answers, which are easily the true go-to solutions for one-liners, but just in case you'd like or need to do it pure-bash only you might start from the following:

{ while read letters _ && [ "${letters}" != four ] ; do :; done ; read && read _ number _ && echo ${number} ; } < data.txt

It only yields the first occurrence of "four".

Here I used _ as the name for a placeholder variable, i.e. a variable that receives values to be discarded, and as an "added bonus" you have the | cut -f2 result already built into the script.

Also please note that above script is just a proof-of-concept: it only works with the exampled input data.

It could be enhanced by:

  • reading the entire (unsplit) line to match it against a regex via the [[ command (in place of [) along with its =~ operator, like in: while read line && ! [[ "${line}" =~ four ]] but beware the then additional complexity given by bash’s own escaping rules if the regex gets more complex than a simple "four"; also note the ! preceding the [[ to negate the test
  • joining the [ (or [[) command with the two read and the && echo ... together inside the while loop, to not stop after first match, like in:
{ while read letters _ ; do [ "${letters}" = four ] && read && read _ number _ && echo ${number} ; done ; } < data.txt

Note also the change of the comparison operator. However beware of missing possibly adjacent matching lines. In order to address this possibility you’d need some kind of look-ahead or back-caching, for which you’d need some more advanced bash constructs.

Lastly, of course you can rather output the entire unsplit line via read line && echo ${line} in place of read _ number _ && echo ${number}

4
  • So you’re assuming that the actual data is exactly like the sample data — the pattern to be searched for is a simple word that appears as a whole word, by itself, as the first word on a line.  But the question doesn’t say that; the sample data should be considered as just an example.  What if the line matching four actually says “I have fourteen cats.”?  What if the user actually wants to do this with a non-trivial regular expression (e.g., fox*ur)? … (Cont’d) Mar 18, 2019 at 20:56
  • (Cont’d) …  What’s worse, the question clearly says that it wants to get the n-th (second) line after the line that matched — not the second word from that line.  Inasmuch as | cut -d' ' -f2 was also given as an example, baking it into your answer doesn’t earn you any points. … … … … … … … … … … … … … … Aside from that, this is a valid contribution to the discussion.  When you fix the above problems, consider showing two versions: one that finds the first match and stops, and another that finds all the matches. Mar 18, 2019 at 20:56
  • @Scott: Well, my "script" is an example too, as much as the question’s information is an example itself - after all I did say "you could start with the following", and I suppose the script does look like just the proof-of-concept it indeed is. But I should make that clearer, agreed. Then, should I provide that much more functionality than explicitly requested ? In order to implement those additional features the script might easily become way more complex than it already is. Wouldn’t that risk to be an overkill ? I think I’d rather wait for explicit inquiries, if and when any (cont'd)
    – LL3
    Mar 18, 2019 at 21:45
  • (cont'd) Wrt the “doing away the cut -f2 part”, it was certainly not for gaining points (nor is my entire contribution as a whole, considering that it is in general quite inferior (ie more complicated) to the other solutions), rather it just cost nothing to do it rather than not, considering that the question mentioned it as a likely subsequent processing of the input data. I contribute with a HTH mindset, I can do without an upvote and am happy with just a “nice, thank you”.
    – LL3
    Mar 18, 2019 at 21:45

You must log in to answer this question.

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