Peeter Joot's (OLD) Blog.

Math, physics, perl, and programming obscurity.

Restricting pattern replacement to specific lines?

Posted by peeterjoot on September 29, 2009

We’ve had a marketing driven name change that impacts a lot of internal error strings in our code in a silly way, and I’ve got a lot of stuff like the following output

# grep -n '".* BA' *C
foo.C:1486:                       "Unexpected BA error - panic",
foo.C:1561:                       "Unexpected BA error - panic",
foo.C:1569:                       "Unexpected BA error",

All these ‘BA’s have to be changed to ‘BF’.

Ideally no customers would ever see these developer centric messages, but they will be in our logfiles, and potentially visible and confusing.

It’s not too hard to replace these, but there’s a lot of them. I’ve had this kind of task before, and have done it using hacky throw away command line “one-liners” like the following:

for i in `cut -f1,2 -d: grepoutput` ; do
   f=`echo $i | cut -f1 -d:`
   l=`echo $i | cut -f2 -d:`
   vim +$l +:'s/\/BF/g' +wq $i

Okay, it’s not a one liner as above since I’ve formatted this with newlines instead of semicolons, but when tossing off throwaway bash/ksh for loop stuff like this I usually do it as a one liner. This bash loop is easy enough to write, but messy and also fairly easy to get wrong. I’m tired of doing this over and over again.

It seemed to me that it was time to code up something that I can tweak for automated tasks like this, and wrote the perl script below that consumes grep -n ouput (ie. file:lineno:stuff output), and makes the desired replacements, whatever they are. I’ve based this strictly on the grep output because the unrestricted replacements could be dangerous and I wanted to visually verify that all the replacement sites were appropriate.


my %lines ;

while (<>)
   chomp ;

   /^(.*?):(\d+?):/ or die "unexpected grep -n output on line '$_'\n" ;

   $lines{$1} .= ",$2" ;

foreach (keys %lines)
   process_file( $_, split(/,/, $lines{$_} ) ) ;

exit ;

sub process_file
   my ($filename, @n) = @_ ;

   my %thisLines = map { $_ => 1 } @n ;

   open my $fhIn, "<$filename" or die "could not open file for input '$filename'\n" ;
   open my $fhOut, ">$filename.2" or die "could not open file for input '$filename.2'\n" ;

   my $lineno = 0 ;
   while ( <$fhIn> )
      $lineno++ ;

      if ( exists($thisLines{$lineno}) )
#print "$filename:$lineno: operating on: '$_'\n" ;
         # word delimiters to replace BA but not BASH nor ABAB, ...
         s/\bBA\b/BF/g ;

      print $fhOut $_ ;

   close $fhIn ;
   close $fhOut ;

This little script, while certainly longer than the one-liner method, is fairly straightforward and easy to modify for other similar ad-hoc replacement tasks later. However, I have to wonder if there’s an easier way?


2 Responses to “Restricting pattern replacement to specific lines?”

  1. Dave said

    Here’s a slightly tweaked version:

    use strict;
    use warnings;

    my %lines ;

    while ()
    /^(.*?):(.*?):/ or die “Don’t recognize ‘$_'” ;

    push @{$lines{$1}}, $2 ;

    process_file( $_, $lines{$_} ) foreach keys %lines;

    exit ;

    sub process_file
    my ($filename, $thislines) = @_ ;

    open IN, ‘<', $filename or die "Can't read '$filename': $!";
    my @lines = ;
    close IN;

    # word delimiters to replace BA but not BASH nor ABAB, …
    $lines[$_-1] =~ s/\bBA\b/BF/g foreach @$thislines;

    open OUT, ‘>’, “$filename.2” or die “Can’t create ‘$filename.2’: $!”;
    print OUT @lines;
    close OUT;

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: