File Coverage

lib/Emacs/Rep.pm
Criterion Covered Total %
statement 73 81 90.1
branch 9 12 75.0
condition 0 3 0.0
subroutine 9 10 90.0
pod 4 4 100.0
total 95 110 86.3


line stmt bran cond sub pod time code
1             package Emacs::Rep;
2             # doom@kzsu.stanford.edu
3             # 15 May 2010
4             # 3 Jun 2012
5              
6             =head1 NAME
7              
8             Emacs::Rep - a find & replace engine for rep.pl and rep.el
9              
10             =head1 SYNOPSIS
11              
12             use Emacs::Rep qw( do_finds_and_reps parse_perl_substitutions );
13              
14             my $substitutions =>>'END_S';
15             s/jerk/iconoclast/
16             s/conniving/shrewd/
17             s/(t)asteless/$1alented/i
18             END_S
19              
20             my $find_replaces_aref =
21             parse_perl_substitutions( \$substitutions );
22              
23             my $change_metatdata_aref =
24             do_finds_and_reps( \$text, $find_replaces_aref );
25              
26             =head1 DESCRIPTION
27              
28             Emacs::Rep is a module that acts as a back-end for the rep.pl
29             script which in turn is used by the emacs library. rep.el.
30              
31             Emacs::Rep is a find and replace engine that can perform
32             multiple perl substitution commands (e.g. s///g) on a given
33             file, recording all metadata about each change so that an an
34             external program (such as emacs) can interactively display
35             and control the changes.
36              
37             The end user isn't expected to need to use these routines
38             directly.
39              
40             An application programmer might use these routines to add
41             support to an interactive front-end (Emacs or otherwise).
42              
43             =head2 EXPORT
44              
45             None by default. Any of the following may be requested (or all
46             with the ':all' tag).
47              
48             =over
49              
50             =cut
51              
52 4     4   485270 use 5.008;
  4         17  
  4         207  
53 4     4   1397 use strict;
  4         640  
  4         171  
54 4     4   25 use warnings;
  4         13  
  4         2450  
55             my $DEBUG = 0;
56 4     4   30 use Carp;
  4         8  
  4         883  
57 4     4   23 use Data::Dumper;
  4         8  
  4         191  
58 4     4   16494 use PPI;
  4         887258  
  4         3845  
59              
60             require Exporter;
61              
62             our @ISA = qw(Exporter);
63             our %EXPORT_TAGS = ( 'all' => [
64             qw(
65             parse_perl_substitutions
66             do_finds_and_reps
67              
68             accumulate_find_reps
69             check_versions
70             ) ] );
71              
72             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
73             our @EXPORT = qw( );
74             our $VERSION = '1.00'; # TODO manually sync-up rep.pl and rep.el versions
75              
76             =item parse_perl_substitutions
77              
78             Breaks down a set of perl substitution command (i.e. "s///;",
79             "s{}{};", etc.) into it's main components (the find pattern
80             and the replace expression). It returns this in an an array
81             of arrays data structure (which is the form used by L).
82              
83             Takes one argument, a scalar reference to a block of text
84             containing one or more perl substitution commands, in any
85             form (PPI is used internally to parse this).
86              
87             The more elaborate "s{}{}xmsg;" is fine, as well as "s///g;".
88              
89             End of line comments (after the closing semicolon) beginning with a "#",
90             are allowed, as are embedded comments inside the find pattern
91             if the /x modifier is in use.
92              
93             Example usage:
94              
95             my $substitutions =>>'END_S';
96             s/pointy-haired boss/esteemed leader/g;
97             s/death spiral/minor adjustment/g;
98             END_S
99              
100             my $find_replaces_aref =
101             parse_perl_substitutions( \$substitutions );
102              
103             Where the returned data should look like:
104              
105             [ ['pointy-haired boss', 'esteemed leader'],
106             ['death spiral', 'minor adjustment'],
107             ]
108              
109             Any trailing modifiers are automatically prefixed to the
110             find_pattern, using the (? ... ) notation, *except* for /g
111             and /e.
112              
113             For purposes of L /e is always ignored
114             (as of this writing), and /g is always assumed, irrespective
115             of whether it was added explicitly.
116              
117             =cut
118              
119             sub parse_perl_substitutions {
120 4     4 1 747 my $reps_text_ref = shift;
121 4         58 my $Document = PPI::Document->new( $reps_text_ref );
122 4         43232 my $s_aref = $Document->find('PPI::Token::Regexp::Substitute');
123 4         6904 my @find_reps;
124 4         13 foreach my $s_obj (@{ $s_aref }) {
  4         14  
125 32         135 my $find = $s_obj->get_match_string;
126 32         504 my $rep = $s_obj->get_substitute_string;
127 32         407 my $modifiers = $s_obj->get_modifiers; # href
128 32         372 my @delims = $s_obj->get_delimiters;
129              
130 32         713 my $raw_mods = join '', keys %{ $modifiers };
  32         108  
131              
132 32         83 accumulate_find_reps( \@find_reps, $find, $rep, $raw_mods );
133             }
134 4         53 \@find_reps;
135             }
136              
137             =item accumulate_find_reps
138              
139             For internal use. Example usage:
140              
141             accumulate_find_reps( \@find_reps, $find, $rep, $raw_mods );
142              
143             =cut
144              
145             sub accumulate_find_reps {
146 32     32 1 39 my $find_reps_aref = shift;
147 32         51 my $find = shift;
148 32         41 my $rep = shift;
149 32         38 my $raw_mods = shift;
150              
151 32 100       99 if ($raw_mods) {
152             # The modifiers we care about (screening out spurious g or e or ;)
153 17         41 my @mods = qw( x m s i );
154 17         20 my $mods = '';
155 17         24 foreach my $m (@mods) {
156 68 100       12767 if ( $raw_mods =~ qr{$m}x ) {
157 5         20 $mods .= $m;
158             }
159             }
160             # modify $find to incorporate the modifiers internally
161 17 100       59 $find = "(?$mods)" . $find if $mods;
162             }
163              
164 32         42 push @{ $find_reps_aref }, [ $find, $rep ];
  32         181  
165             }
166              
167             =item do_finds_and_reps
168              
169             Does a series of finds and replaces on some text and
170             returns the beginning and end points of each of the
171             modfied regions, along with some other information about
172             the matches.
173              
174             Takes two arguments:
175              
176             (1) The text to be modified, usually as a reference, though a scalar is okay
177             (2) A series of find and replace pairs in the form
178             of an aref of arefs, e.g.
179              
180             $find_replaces_aref =
181             [ ['jerk', 'iconoclast'],
182             ['conniving', 'shrewd'].
183             ['(?i)(t)asteless', '$1alented'].
184             ]:
185              
186             (See L.)
187              
188             Example usage:
189              
190             $locations_aref =
191             do_finds_and_reps( \$text, $find_replaces_aref );
192              
193             The returned change metadata is an aref of arefs of hrefs;
194             an array of passes with an entry for each substitution pair,
195             an an array of changes made by each pass. The href has
196             keys: 'beg', 'delta', 'orig', 'rep'.
197              
198             The fields 'orig' and 'rep' contain the modified string, before
199             and after the change.
200              
201             'delta' is the change in length due to the change.
202              
203             'beg' is the beginning of the region that was modified, an
204             integer counting from the start of the file, where the first
205             character is 1.
206              
207             This numbering does not change while a s///g is in progress,
208             even if it is changing the length of the strings.
209             And further, these change locations are recorded *during* each pass,
210             which means that later passes throw off the numbering.
211              
212             In practice, for the rep.el application, we apply this data
213             on the emacs side in inverse order, so that the numbering is
214             correct in the context we use it.
215              
216             Note, error messages are routed to stdout, labeled with the
217             prefix "Problem:". The elisp call shell-command-to-string
218             merges stdout and stderr, but we use the 'Problem' prefix to
219             spot error messages
220              
221             =cut
222              
223             sub do_finds_and_reps {
224 3     3 1 2122 my $arg = shift;
225 3 50       15 my $text_ref = ref( $arg ) ? $arg : \$arg;
226 3         7 my $find_replaces = shift; # aref of aref: a series of pairs
227              
228 3         5 my @change_metadata;
229 3         7 eval {
230 3         53 for ( my $pass = 0; $pass <= $#{ $find_replaces }; $pass++ ) {
  11         38  
231 9         13 my ($find_pat, $replace) = @{ $find_replaces->[ $pass ] };
  9         22  
232 9         13 my @pass; # change_metadata for this pass
233 9         38 ${ $text_ref } =~
  9         186  
234             s{$find_pat}
235             {
236 9         493 my $new = eval "return qq{$replace}";
237 9         28 my $l1 = length( $& );
238 9         11 my $l2 = length( $new );
239 9         14 my $delta = $l2 - $l1;
240             # pos points at the *start* of the match (inside of a s///eg)
241             # And char numbering fixed at the start of the s///ge run
242 9         12 my $p = pos( ${ $text_ref } ) + 1;
  9         16  
243 9         14 my $beg = $p;
244              
245             # preserving some context
246 9         21 my $post = substr( $', 0, 10 ); # Note: no BOF/EOF errors
247 9         22 my $pre = substr( $`, -10 );
248              
249 9         44 push @pass, {
250             beg => $beg,
251             delta => $delta,
252             orig => $&,
253             rep => $new,
254             };
255 9         34 $new
256             }ge;
257 8         44 push @change_metadata, \@pass;
258             }
259             };
260 3 100       15 if ($@) {
261             # Send error message to STDOUT so that it won't mess up test output.
262             # (and anyway, the elisp call shell-command-to-string merges in STDERR)
263             #
264             # The elisp function rep-run-perl-substitutions uses prefix "Problem".
265             # to spot error messages
266 1         72 print "Problem: $@\n";
267             # roll-back
268 1         4 @change_metadata = ();
269             }
270              
271 3         13 return \@change_metadata; # array of array of hrefs (keys: beg, end, delta, orig, etc)
272             }
273              
274              
275             =item check_versions
276              
277             Verify that all three pieces of the system have the same version:
278             the *.pl, *.pm and *.el.
279              
280             The elisp code is expected to run this command string with
281             it's own version number:
282              
283             perl rep.pl --check_versions="0.08"
284              
285             rep.pl then runs this check_versions routine, passing along the
286             elisp version and the rep.pl version number:
287              
288             Example usage:
289              
290             check_versions( $elisp_version, $script_version );
291              
292             This compares those two versions with the module's version,
293             and warns if they're not all the same.
294              
295             =cut
296              
297             sub check_versions {
298 0     0 1   my $elisp_version = shift;
299 0           my $script_version = shift;
300 0           my $module_version = $VERSION;
301              
302 0           my $mess;
303 0 0 0       if ( not(
304             ($elisp_version == $script_version) &&
305             ($script_version == $module_version) ) ) {
306 0           $mess = "Warning: all three versions should match: \n" .
307             "rep.el: $elisp_version \n" .
308             "rep.pl: $script_version \n" .
309             "Rep.pm: $module_version \n";
310 0           return $mess;
311             } else {
312 0           return $module_version;
313             }
314             }
315              
316              
317              
318             1;
319              
320             =back
321              
322             =head1 SEE ALSO
323              
324             The web page for this project is:
325              
326             http://obsidianrook.com/rep
327              
328             The code is available on github (as well as on CPAN):
329              
330             http://github.com/doomvox/rep
331              
332             Emacs::Rep is the back-end for the script rep.pl which
333             in turn is the back-end for the emacs lisp code rep.el.
334              
335             If rep.el is not installed, look in the "elisp" sub-directory
336             of this CPAN package.
337              
338             A good discussion forum for projects such as this is:
339              
340             http://groups.google.com/group/emacs-perl-intersection
341              
342             =head1 AUTHOR
343              
344             Joseph Brenner, Edoom@kzsu.stanford.eduE
345              
346             =head1 COPYRIGHT AND LICENSE
347              
348             Copyright (C) 2010,2012 by Joseph Brenner
349              
350             This program is free software; you can redistribute it and/or modify it
351             under the terms of either: the GNU General Public License as published
352             by the Free Software Foundation; or the Artistic License.
353              
354             See http://dev.perl.org/licenses/ for more information.
355              
356             =head1 BUGS
357              
358             None reported... yet.
359              
360             =cut