#!/usr/bin/perl # TODO option to read file names from STDIN # TODO options to control generation of backups # TODO -S, --suffix=SUFFIX override the usual backup suffix ? # TODO guard against mis-escaped lines use File::Temp qw(tempfile); use Getopt::Long; use Pod::Usage; GetOptions(\%options, qw( C|context=i editor=s help|h|? )) or pod2usage(2); pod2usage(1) if $options{help}; $options{C} ||= 2; $options{editor} ||= $ENV{EDITOR} || 'vim'; pod2usage "?Argument to --context must not be negative." if $options{C} < 0; $re = shift @ARGV or pod2usage "?Missing regular expression."; $re = qr/$re/o; # TODO right thing? @ARGV = grep { !m/\n/ or warn "!ignoring file `$_' with newline(s) in its name.\n" and 0 } @ARGV; pod2usage "?Missing file names." unless @ARGV; ($fh,$edits) = tempfile( 'matcheditXXXXX', DIR => File::Spec->tmpdir ); sub process_hit { # $HIT[0] is line $first # $HIT[$lasthit-$first] is line $lasthit # $HIT[$lasthit-$first+$options{C}$ is line $lasthit+$options{C} # we have to print the lines from $first to ($lasthit+$options{C}), possibly less if at end of file my $lastidx = $lasthit-$first+$options{C}; $anyhit = 1; $lastidx = $#HIT if $lastidx > $#HIT; my $last = $first+$lastidx; print $fh "LINES $first-$last\n", splice( @HIT,0,$lastidx+1 ), "LINESEND\n"; $lasthit = undef; # $first is updated in main program... } while (<>) { if ($.==1) { process_hit if $lasthit; print $fh "FILEEND\n" if $fileheader; @HIT = (); $fileheader = 0; $first = 1; # first line collected in @HIT will have number 1 } push @HIT, $_; # collect if( /$re/ ) { $lasthit = $.; unless( $fileheader ) { print $fh "FILE $ARGV\n"; $fileheader = 1; }; } if( $lasthit ) { # running hit if( $.-$lasthit > 2*$options{C} ) { # we now have 2*$options{C}+1 intervening lines between last hit and current line process_hit; shift @HIT; # throw away one more, then we still have $options{C} lines of leading context $first = $.-$options{C}+1; } } elsif( @HIT > $options{C} ) { # throw one line away shift @HIT; $first++; } } continue { close ARGV if eof; # reset $. } process_hit if $lasthit; print $fh "FILEEND\n" if $fileheader; unless( $anyhit ) { close $fh; unlink $edits; die "!No matching file.\n"; } system $options{editor}, $edits; # TODO check time stamp if there was any modification at all print "Answer YES if you want the changes to take effect: "; $choice = ; if( $choice eq "YES\n" ) { seek $fh, 0, SEEK_SET; # start over while( <$fh> ) { last unless m/^FILE (.*)/; $fn = $1; system( "mv $fn $fn.bak" ); open( PROCIF, "<$fn.bak" ); open( OF, ">$fn" ); while( <$fh> ) { last unless m/^LINES (\d+)-(\d+)/; $from = $1; $to = $2; tell PROCIF; # to initialize $. while( $. < $from - 1 ) { # copy part before print OF scalar ; } while( $. < $to ) { # dummy read the part ; } while( <$fh> ) { # paste in new lines last if /^LINESEND/; print OF $_; } last if /^FILEEND/; } # must have read FILEEND on correct file structure print OF ; # copy rest of file close PROCIF; } } close $fh; unlink $edits; __END__ =head1 NAME matchedit - edit regions of files that match a (Perl) regular expression in a single session =head1 SYNOPSIS B [B<--context> I] [B<--editor> I] I I... =head1 DESCRIPTION B allows you to edit the regions of the specified files that match the (Perl) regular expression I. =head1 OPTIONS =over 4 =item B<--context> I, B<-C> I Print I lines of context before and after each match. =item B<--editor> I Use I to edit the matches. If this option is not given the editor defaults to the one specified with environment variable I or vim. =back =head1 BUGS AND CAVEATS The markup of the file holding the matches is neither nice nor fool-proof. This means that your edited section may contain markup that confuses this script. This is not guarded against (yet), but for each changed file the script generates a backup copy, so in case of unexpected trouble you may recover. =head1 COPYRIGHT (C) 2000-2005 Mark Hillebrand . This code is released under the BSD License. Before using this software, visit L for the full license text.