87

Question: I'd like to print a single line directly following a line that contains a matching pattern.

My version of sed will not take the following syntax (it bombs out on +1p) which would seem like a simple solution:

sed -n '/ABC/,+1p' infile 

I assume awk would be better to do multiline processing, but I am not sure how to do it.

1

9 Answers 9

192

Never use the word "pattern" on it's own in this context as it is ambiguous. Always use "string" or "regexp" (or in shell "globbing pattern"), whichever it is you really mean. See How do I find the text that matches a pattern? for more about that.

The specific answer you want is:

awk 'f{print;f=0} /regexp/{f=1}' file 

or specializing the more general solution of the Nth record after a regexp (idiom "c" below):

awk 'c&&!--c; /regexp/{c=1}' file 

The following idioms describe how to select a range of records given a specific regexp to match:

a) Print all records from some regexp:

awk '/regexp/{f=1}f' file 

b) Print all records after some regexp:

awk 'f;/regexp/{f=1}' file 

c) Print the Nth record after some regexp:

awk 'c&&!--c;/regexp/{c=N}' file 

d) Print every record except the Nth record after some regexp:

awk 'c&&!--c{next}/regexp/{c=N}1' file 

e) Print the N records after some regexp:

awk 'c&&c--;/regexp/{c=N}' file 

f) Print every record except the N records after some regexp:

awk 'c&&c--{next}/regexp/{c=N}1' file 

g) Print the N records from some regexp:

awk '/regexp/{c=N}c&&c--' file 

I changed the variable name from "f" for "found" to "c" for "count" where appropriate as that's more expressive of what the variable actually IS.

f is short for found. Its a boolean flag that I'm setting to 1 (true) when I find a string matching the regular expression regexp in the input (/regexp/{f=1}). The other place you see f on its own in each script it's being tested as a condition and when true causes awk to execute its default action of printing the current record. So input records only get output after we see regexp and set f to 1/true.

c && c-- { foo } means "if c is non-zero then decrement it and if it's still non-zero then execute foo" so if c starts at 3 then it'll be decremented to 2 and then foo executed, and on the next input line c is now 2 so it'll be decremented to 1 and then foo executed again, and on the next input line c is now 1 so it'll be decremented to 0 but this time foo will not be executed because 0 is a false condition. We do c && c-- instead of just testing for c-- > 0 so we can't run into a case with a huge input file where c hits zero and continues getting decremented so often it wraps around and becomes positive again.

Sign up to request clarification or add additional context in comments.

4 Comments

I'm using c) Print the Nth record after some regexp to summarise test results (i.e. the test fail detail is two lines after the test suite name that starts with '●'), but I noticed there's some occasions I need to instead print out 4 lines below, if the second line matches a string. Is there a way to work that in, or is it worth another question?
@AncientSwordRage It's always worth asking another question rather than trying to get an answer in a comment.
I wanted to make sure it wasn't something blindingly obvious I'd missed. As it wasn't I asked stackoverflow.com/questions/74603638/…
This answer taught me interesting and useful things about awk. In particular, the use of a boolean variable.
59

It's the line after that match that you're interesting in, right? In sed, that could be accomplished like so:

sed -n '/ABC/{n;p}' infile 

Alternatively, grep's A option might be what you're looking for.

-A NUM, Print NUM lines of trailing context after matching lines. 

For example, given the following input file:

foo bar baz bash bongo 

You could use the following:

$ grep -A 1 "bar" file bar baz $ sed -n '/bar/{n;p}' file baz 

5 Comments

Note: {n;p} seems to be supported by GNU sed but not BSD sed. (Thanks for a sed answer chooban. I have great respect for awk, and have used it, but I try avoid relearning its baroque language whenever possible. (When I need awk I use perl).)
Correction: I had success with BSD sed by adding a ;: sed -n /bar/{n;p;}. Works with GNU sed as well.
With the original sed you would've had to write sed -n '/bar/{;n;p;}' because { and } were parsed exactly the same as letter commands.
It might be helpful to add a brief description of what the {n;p} is doing in conjunction with the -n to only print the line after the pattern. As far as I understand it, the {n adds the next line into the pattern-space, then the ;p prints it, meanwhile -n makes no other lines print.
Most portable is to pass each line of the sed program as a separate -e argument: sed -n -e '/bar/{' -e 'n' -e 'p' -e '}'
8

I needed to print ALL lines after the pattern ( ok Ed, REGEX ), so I settled on this one:

sed -n '/pattern/,$p' # prints all lines after ( and including ) the pattern 

But since I wanted to print all the lines AFTER ( and exclude the pattern )

sed -n '/pattern/,$p' | tail -n+2 # all lines after first occurrence of pattern 

I suppose in your case you can add a head -1 at the end

sed -n '/pattern/,$p' | tail -n+2 | head -1 # prints line after pattern 

And I really should include tlwhitec's comment in this answer (since their sed-strict approach is the more elegant than my suggestions):

sed '0,/pattern/d' 

The above script deletes every line starting with the first and stopping with (and including) the line that matches the pattern. All lines after that are printed.

2 Comments

There's a sed-strict way for your second case: sed '0,/regex/d'
The above is exactly why I advise people to never use range expressions (/start/,/end/) which means you don't do tasks like this with sed since it doesn't have variables so all you're left with is ranges. All you wanted to do was exclude the start or end line and it required you to add pipes and additional commands to do it vs awk '/start/{f=1} f; /end/{f=0}' - just rearrange the blocks to print or not the start/end sections, no extra tools or pipes required.
3

awk Version:

awk '/regexp/ { getline; print $0; }' filetosearch 

2 Comments

Thanks! I forgot about the -A option in grep; it works perfectly with the +1 parameter (the line with the matched pattern is not printed).
This will fail in cryptic ways when you least expect it, and will be difficult to enhance in future. Make sure you ready and fully understand awk.info/?tip/getline before deciding to use getline.
2

Actually sed -n '/pattern/{n;p}' filename will fail if the pattern match continuous lines:

$ seq 15 |sed -n '/1/{n;p}' 2 11 13 15 

The expected answers should be:

2 11 12 13 14 15 

My solution is:

$ sed -n -r 'x;/_/{x;p;x};x;/pattern/!s/.*//;/pattern/s/.*/_/;h' filename 

For example:

$ seq 15 |sed -n -r 'x;/_/{x;p;x};x;/1/!s/.*//;/1/s/.*/_/;h' 2 11 12 13 14 15 

Explains:

  1. x;: at the beginning of each line from input, use x command to exchange the contents in pattern space & hold space.
  2. /_/{x;p;x};: if pattern space, which is the hold space actually, contains _ (this is just a indicator indicating if last line matched the pattern or not), then use x to exchange the actual content of current line to pattern space, use p to print current line, and x to recover this operation.
  3. x: recover the contents in pattern space and hold space.
  4. /pattern/!s/.*//: if current line does NOT match pattern, which means we should NOT print the NEXT following line, then use s/.*// command to delete all contents in pattern space.
  5. /pattern/s/.*/_/: if current line matches pattern, which means we should print the NEXT following line, then we need to set a indicator to tell sed to print NEXT line, so use s/.*/_/ to substitute all contents in pattern space to a _(the second command will use it to judge if last line matched the pattern or not).
  6. h: overwrite the hold space with the contents in pattern space; then, the content in hold space is ^_$ which means current line matches the pattern, or ^$, which means current line does NOT match the pattern.
  7. the fifth step and sixth step can NOT exchange, because after s/.*/_/, the pattern space can NOT match /pattern/, so the s/.*// MUST be executed!

1 Comment

Perhaps clearer to show as an actual sed -f program file (with proper comments) rather than all bunched up as a -e argument (with non-standard ; joining lines).
1

If pattern match, copy next line into the pattern buffer, delete a return, then quit -- side effect is to print.

sed '/pattern/ { N; s/.*\n//; q }; d' 

2 Comments

q is by no means a GNU extension. It is a standard sed command.
Or use p instead of q if you want want to print all matches, not just the first one.
1

This might work for you (GNU sed):

sed -n ':a;/regexp/{n;h;p;x;ba}' file 

Use seds grep-like option -n and if the current line contains the required regexp replace the current line with the next, copy that line to the hold space (HS), print the line, swap the pattern space (PS) for the HS and repeat.

Comments

0

Piping some greps can do it (it runs in POSIX shell and under BusyBox):

cat my-file | grep -A1 my-regexp | grep -v -- '--' | grep -v my-regexp 
  1. -v will show non-matching lines
  2. -- is printed by grep to separate each match, so we skip that too

Comments

0

If you just want the next line after a pattern, this sed command will work

sed -n -e '/pattern/{n;p;}' 

-n supresses output (quiet mode); -e denotes a sed command (not required in this case); /pattern/ is a regex search for lines containing the literal combination of the characters pattern (Use /^pattern$/ for line consisting of only of “pattern”; n replaces the pattern space with the next line; p prints;

For example:

seq 10 | sed -n -e '/5/{n;p;}' 

Note that the above command will print a single line after every line containing pattern. If you just want the first one use sed -n -e '/pattern/{n;p;q;}'. This is also more efficient as the whole file is not read.

This strictly sed command will print all lines after your pattern.

sed -n '/pattern/,${/pattern/!p;} 

Formatted as a sed script this would be:

/pattern/,${ /pattern/!p } 

Here’s a short example:

seq 10 | sed -n '/5/,${/5/!p;}' 

/pattern/,$ will select all the lines from pattern to the end of the file.

{} groups the next set of commands (c-like block command)

/pattern/!p; prints lines that doesn’t match pattern. Note that the ; is required in early versions, and some non-GNU, of sed. This turns the instruction into a exclusive range - sed ranges are normally inclusive for both start and end of the range.

To exclude the end of range you could do something like this:

sed -n '/pattern/,/endpattern/{/pattern/!{/endpattern/d;p;}} 
/pattern/,/endpattern/{ /pattern/!{ /endpattern/d p } } 

/endpattern/d is deleted from the “pattern space” and the script restarts from the top, skipping the p command for that line.

Another pithy example:

seq 10 | sed -n '/5/,/8/{/5/!{/8/d;p}}' 

If you have GNU sed you can add the debug switch:

seq 5 | sed -n --debug '/2/,/4/{/2/!{/4/d;p}}' 

Output:

SED PROGRAM: /2/,/4/ { /2/! { /4/ d p } } INPUT: 'STDIN' line 1 PATTERN: 1 COMMAND: /2/,/4/ { COMMAND: } END-OF-CYCLE: INPUT: 'STDIN' line 2 PATTERN: 2 COMMAND: /2/,/4/ { COMMAND: /2/! { COMMAND: } COMMAND: } END-OF-CYCLE: INPUT: 'STDIN' line 3 PATTERN: 3 COMMAND: /2/,/4/ { COMMAND: /2/! { COMMAND: /4/ d COMMAND: p 3 COMMAND: } COMMAND: } END-OF-CYCLE: INPUT: 'STDIN' line 4 PATTERN: 4 COMMAND: /2/,/4/ { COMMAND: /2/! { COMMAND: /4/ d END-OF-CYCLE: INPUT: 'STDIN' line 5 PATTERN: 5 COMMAND: /2/,/4/ { COMMAND: } END-OF-CYCLE: 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.