I have the following code that will remove lines with the pattern banana and 2 lines after it:
sed '/banana/I,+2 d' file
So far, so good! But I need it to remove 2 lines before banana, but I can’t get it with a “minus sign” or whatever (similar to what grep -v -B2 banana file should do but doesn’t):
<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="6f1b0a1d0a1c0e0a051a0106001d2f03000c0e0307001c1b">[email protected]</a> ~ > LC_ALL=C sed '-2,/banana/I d' file sed: invalid option -- '2' <a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="65110017001604000f100b0c0a1725090a0604090d0a1611">[email protected]</a> ~ > LC_ALL=C sed '/banana/I,-2 d' file sed: -e expression #1, char 16: unexpected `,' <a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="87f3e2f5e2f4e6e2edf2e9eee8f5c7ebe8e4e6ebefe8f4f3">[email protected]</a> ~ > LC_ALL=C sed '/banana/I,2- d' file sed: -e expression #1, char 17: unknown command: `-'
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
Sed doesn’t backtrack: once it’s processed a line, it’s done. So “find a line and print the previous N lines” isn’t going to work as is, unlike “find a line and print the next N lines” which is easy to graft on.
If the file isn’t too long, since you seem to be ok with GNU extensions, you can use tac to reverse the lines of the file.
tac | sed '/banana/I,+2 d' | tac
Another angle of attack is to maintain a sliding window in a tool like awk. Adapting from Is there any alternative to grep’s -A -B -C switches (to print few lines before and after )? (warning: minimally tested):
#!/bin/sh
{ "exec" "awk" "-f" "$0" "<a href="https://getridbug.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="1e3a5e">[email protected]</a>"; } # -*-awk-*-
# The array h contains the history of lines that are eligible for being "before" lines.
# The variable skip contains the number of lines to skip.
skip { --skip }
match($0, pattern) { skip = before + after }
NR > before && !skip { print NR h[NR-before] }
{ delete h[NR-before]; h[NR] = $0 }
END { if (!skip) {for (i=NR-before+1; i<=NR; i++) print h[i]} }
Usage: /path/to/script -v pattern='banana' -v before=2
Method 2
This is pretty easy with ex or vim -e
vim -e - $file <<@@@ g/banana/.-2,.d wq @@@
The expression reads: for every line containing banana in the range from the current line -2 to the current line, delete.
What’s cool is that the range can also contain backwards and forwards searches, for example this will delete all sections of the file starting with a line containing apple and ending with a line containing orange and containing a line with banana:
vim -e - $file <<@@@ g/banana/?apple?,/orange/d wq @@@
Also note that up to ten vim/ex commands can be submitted using the inline command option “-c”. See the man page.
vim -e -c 'g/banana/.-2,.d' -c 'wq' $yourfilename
and
ex -c 'g/banana/?apple?,/orange/d' -c 'wq' $yourfilename
Method 3
You can do this fairly simply with sed:
printf %s\n 1 2 3 4match 5match 6
7match 8 9 10 11match |
sed -e'1N;$!N;/n.*match/!P;D'
I don’t know why anyone would say otherwise, but to find a line and print previous lines sed incorporates the built-in Print primitive which writes only up to the first newline character in pattern space. The complementary Delete primitive removes that same segment of pattern space before recursively recycling the script with what remains. And to round it off, there is a primitive for appending the Next input line to pattern space following an inserted newline character.
So that one line of sed should be all you need. You just replace match with whatever your regexp is and you’re golden. That should be a very fast solution as well.
Note also that it will correctly count a match immediately preceding another match as both a trigger to quiet output for the previous two lines and quiet its print as well:
1
7match
8
11match
In order for it to work for an arbitrary number of lines, all you need to do is get a lead.
So:
printf %s\n 1 2 3 4 5 6 7match
8match 9match 10match
11match 12 13 14 15 16
17 18 19 20match |
sed -e:b -e'$!{N;2,5bb' -e} -e'/n.*match/!P;D'
1 11match 12 13 14 20match
…deletes the 5 lines preceding any match.
Method 4
Using the “sliding window” in perl:
perl -ne 'push @lines, $_;
splice @lines, 0, 3 if /banana/;
print shift @lines if @lines > 2
}{ print @lines;'
Method 5
Using man 1 ed:
str='
1
2
3
banana
4
5
6
banana
8
9
10
'
# using Bash
cat <<-'EOF' | ed -s <(echo "$str") | sed -e '1{/^$/d;}' -e '2{/^$/d;}'
H
0i
.
,g/banana/km
'm-2,'md
,p
q
EOF
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0