简体   繁体   中英

how i can comment (#) 6 lines before and after the matched pattern in sed

I want to comment(#) 6 lines before and after the matched pattern. I referred this question.

How do I delete a matching line, the line above and the one below it, using sed?

I tried to use hold buffer for this solution, but not working.

I have the following sequence occurring multiple times in a file:

aaaa  
bbbb  
cccc  
dddd  
eeee  
ffff  
gggg  
hhhh  
iiii  
jjjj  
kkkk  
llll  
mmmm  
nnnn  
oooo  

If I searched hhhh , then the output file should be given below:

  aaaa  
  #bbbb  
  #cccc  
  #dddd  
  #eeee  
  #ffff  
  #gggg  
  #hhhh  
  #iiii  
  #jjjj  
  #kkkk  
  #llll  
  #mmmm  
  #nnnn  
  oooo  

Please help me to do this with sed or any other scripts!!!

The question is tagged Vim, so… my beloved :help :global and :help :normal to the rescue!

:g/hhhh/-6,+6norm I#

:substitute variant:

:g/hhhh/-6,+6s/^/#

Breakdown:

  • The :global command is used to execute an Ex command for each line matching the given pattern.

    :g/hhhh/d would delete every line containing hhhh .

  • Ex commands usually accept an optional range. A range can use absolute line numbers, 5,15 and/or relative line numbers, -3,+41 .

    :g/hhhh/-6,+6d would delete everything between 6 lines above and 6 lines below every line containing hhhh .

  • The :normal command allows us to execute normal commands from the command-line and it accepts a range, like the other Ex commands. I# is the simplest way to insert a # at the beginning of a line so we can do :normal I# from the command-line, which brings us to the first solution:

     :g/hhhh/-6,+6norm I# 
  • As an Ex command, :substitute also accepts a range so we can use it as well to insert a # at the beginning of each line in the range, which brings us to the second solution:

     :g/hhhh/-6,+6s/^/# 

To do this in Perl, you need to read the entire file into an array, then find the index of the matching line and edit the surrounding lines, which can be done with a range.

You must remove undefined values from the array slice, or you will create new entries if your match is near the beginning or end of the file (ie less than 6 lines away).

perl -we '@a = <>;                      # read whole file
           for (0 .. $#a) {              # loop over indexes
               if ($a[$_] =~ /hhhh/) {   # find match
                   s/^/#/ for grep defined, @a[$_-6 .. $_+6]  # edit
               } 
           }; print @a" hhh.txt

In a for loop $_ is aliased to the elements, which is why we can directly apply a substitution s/// to it.

This can also possibly be simplified by using Tie::File .

Output:

aaaa
#bbbb
#cccc
#dddd
#eeee
#ffff
#gggg
#hhhh
#iiii
#jjjj
#kkkk
#llll
#mmmm
#nnnn
oooo

这可能对您有用(GNU sed):

sed -r ':a;s/\n/&/6;tb;$!{N;ba};:b;/SEARCH_STRING/!{P;D};s/\n/&/12;tc;$!{N;bb};:c;s/^/#/gm' file

** here comes a Perl solution!! **

I would store the whole content in a Array, iterating through the array and flag the iterator variable if the pattern matches. Then remove and add 6 from the iterator variable and there we go, if the flagged variables are given, you can concat the hashtag in front of the line.

to make it more clearly:

use File::Slurp;
my $find_counter = 0;
my $line_counter = 0;
my @lines = read_file( 'filename' ) ;
foreach my $line (@lines) { # foreach or for loop
  if ($line =~ /$pattern/) {
    $file_counter = $line_counter;
    last;
  }
  $line_counter++;
}
# loop again through @lines and when the line is between
# $file_counter + - 6 , concat the hashtag in front of the line

Another way reads one line at a time which may be better for large files, it avoids reading the whole file into memory.

The array @prev_lines hold the number of lines that are to be printed before the match. When a match is found, print the remembered lines with the # prefix and set $num_line_to_print to the number of lines to print after the match. If the line does not match then see whether lines are to be printed for a previous match. If neither then push the line onto the array in case it matches in the future. If the array now has too many lines, they were not close to the match so just print them. Finally, after the while loop just print out any save lines.

use strict;
use warnings;

my $num_lines_wanted = 6;

my @prev_lines;
my $num_line_to_print = 0;

while ( <> ) {
    if ( m/hhhh/ ) {
        while ( scalar(@prev_lines) > 0 ) {
            print "#", shift @prev_lines;
        }
        print "#", $_;
        $num_line_to_print = $num_lines_wanted;
    }
    elsif ( $num_line_to_print > 0 ) {
        print "#", $_;
        $num_line_to_print--;
    }
    else {
        push @prev_lines, $_;
        if ( scalar(@prev_lines) > $num_lines_wanted ) {
            print shift @prev_lines;
        }
    }
}

while ( scalar(@prev_lines) > 0 ) {
    print shift @prev_lines;
}

The original question is not clear how to handle input where two hhhh lines are within the six lines of each other. The code here restarts the numbering at each match, it prints input lines only once and only adds one # even when a line is within the range of two hhhh matches.

For those who are comfortable with sed, I suggest using grep to get the context piped to sed to create some simple sed commands:

grep -A6 -B6 -n hhhh file | sed -e 's/[^0-9].*$//' -e 's/$/s|^|#|/' | sed -f- file

(The examples below have -A1 and -B1 to shorten the length of this output.)

Get line-numbered -A1 one line after and -B1 one line before the matching line with grep -A1 -B1 -n hhhh file which outputs:

7-gggg  
8:hhhh  
9-iiii

... which we're going to turn into sed commands to comment out those numbered lines with | sed -e 's/[^0-9].*$//' -e 's/$/s|^|#|/' | sed -e 's/[^0-9].*$//' -e 's/$/s|^|#|/' , two sed commands to remove everything after the first non-digit and replace the end of that shortened line with | sed -e 's|^|#|' | sed -e 's|^|#|' , the replacement which comments out the entire line. That gets this:

7s|^|#|
8s|^|#|
9s|^|#|

... and we want to pipe those commands to sed, so we use -f- which is equivalent to -f /dev/stdin and instructs sed to read commands from stdin.

grep -A1 -B1 -n hhhh abcd.txt | sed -e 's/[^0-9].*$//' -e 's/$/s|^|#|/' | sed -f- abcd.txt

aaaa  
bbbb  
cccc  
dddd  
eeee  
ffff  



jjjj  
kkkk  
llll  
mmmm  
nnnn  
oooo

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM