File Coverage

blib/lib/persona.pm
Criterion Covered Total %
statement 35 130 26.9
branch 10 82 12.2
condition 2 19 10.5
subroutine 8 14 57.1
pod 0 1 0.0
total 55 246 22.3


line stmt bran cond sub pod time code
1             package persona;
2              
3             $VERSION= '0.15';
4              
5             # be as strict and verbose as possible
6 1     1   58441 use strict;
  1         2  
  1         24  
7 1     1   4 use warnings;
  1         1  
  1         23  
8              
9             # modules that we need
10 1     1   4 use List::Util qw( first );
  1         2  
  1         155  
11              
12             # persona and semaphore to indicate @INC watcher is installed for this process
13             my $process_persona;
14              
15             # regular expression to check
16             my $all;
17             my @only_for;
18              
19             # are we debugging?
20             BEGIN {
21 1   50 1   8 my $debug= $ENV{DEBUG} || 0;
22 1 50       6 $debug= 0 if $debug !~ m#^[0-9]+$#; ## only numeric constants
23 1         103 eval "sub DEBUG () { $debug }";
24             } #BEGIN
25              
26             # log pipe if we're debugging
27             BEGIN {
28             *TELL= DEBUG
29             ? sub {
30             my $format = shift() . "\n";
31             printf STDERR $format, @_;
32             }
33 1     1   524 : sub { };
        0      
34             } #BEGIN
35              
36             # satisfy -require-
37             1;
38              
39             #-------------------------------------------------------------------------------
40             #
41             # Class methods
42             #
43             #-------------------------------------------------------------------------------
44             # path2source
45             #
46             # Convert a given handle to source code for a given persona
47             #
48             # IN: 1 class (ignored)
49             # 2 path
50             # 3 persona to parse for (default: current)
51             # OUT: 1 scalar reference to source code or undef
52             # 2 number of lines skipped (optional)
53              
54             sub path2source {
55 0     0 0 0 my ( undef, $path, $persona )= @_;
56 0 0       0 $persona= $process_persona if !defined $persona;
57              
58             # could not open file, let the outside handle this
59 0 0       0 open( my $handle, '<', $path ) or return;
60 0         0 TELL 'Parsing %s', $path if DEBUG;
61              
62             # no persona, so we don't need to do any parsing at all
63 0 0       0 if ( !$persona ) {
64              
65             # enable slurp mode
66 0         0 local $/;
67 0         0 my $source= readline $handle;
68              
69             # we're done
70 0 0       0 return wantarray ? ( \$source, 0 ) : \$source;
71             }
72              
73             # initializations
74 0         0 my $done;
75 0         0 my $skipped= 0;
76 0         0 my $active= 1;
77 0         0 my $line_nr= 0;
78 0         0 my $source= '';
79              
80             # until we reach end of file
81             LINE:
82 0         0 while (1) {
83 0         0 my $line= readline $handle;
84 0 0       0 last LINE if !defined $line;
85              
86             # seen __END__ or __DATA__, no further looking needed here
87 0 0       0 if ($done) {
    0          
88 0         0 $source .= $line;
89 0         0 next LINE;
90             }
91              
92             # reached the end of logical code, continue without further looking
93             elsif ( $line =~ m#^__(?:DATA|END)__$# ) { ## syn hilite
94 0         0 $done= 1;
95 0         0 $source .= $line;
96 0         0 next LINE;
97             }
98              
99             # we've seen a line and want to remember that
100 0         0 $line_nr++;
101              
102             # code for new persona?
103 0 0       0 if ( $line =~ m/^#PERSONA\s*(.*)/ ) {
104 0         0 my $rest= $1;
105              
106             # all personas
107 0 0       0 if ( !$rest ) {
108              
109             # switching from inactive persona to all
110 0 0       0 if ( !$active ) {
111 0         0 $active= 1;
112              
113             # make sure errors / stack traces have right line info
114 0         0 $source .= sprintf "#line %d %s (all personas)\n",
115             $line_nr + 1, $path;
116              
117             # don't bother adding the line with #PERSONA
118 0         0 next LINE;
119             }
120             }
121              
122             # we have an expression
123             else {
124              
125             # huh?
126 0 0       0 die "Found illegal characters in PERSONA specification:\n$rest"
127             if $rest =~ m#[^\w\s\(\)\|!]#;
128              
129             # change simple list into expression
130 0         0 1 while $rest =~ s#(?
131              
132             # create evallable expression
133 0         0 my %value= ( $persona => 1 );
134 0 0       0 $rest =~ s#(\w+)# $value{$1} || 0 #ge;
  0         0  
135              
136             # evaluate expression
137 0         0 my $ok= eval $rest;
138 0 0       0 die "Error in evaluation persona specification:\n'$rest'\n$@"
139             if $@;
140              
141             # stop copying code for now
142 0 0       0 if ( !$ok ) {
    0          
143 0         0 $active= undef;
144             }
145              
146             # switching from inactive persona to all
147             elsif ( !$active ) {
148 0         0 $active= 1;
149              
150             # make sure errors / stack traces have right line info
151 0         0 $source .=
152             sprintf "#line %d %s (allowed by persona '%s')\n",
153             $line_nr + 1, $path, $persona;
154              
155             # don't bother adding the line with #PERSONA
156 0         0 next LINE;
157             }
158             }
159             }
160              
161             # we're not doing this line
162 0 0       0 $skipped++, next LINE if !$active;
163              
164             # new package, make sure it knows about PERSONA if it doesn't yet
165 0 0       0 if ( $line =~ m#^\s*package\s+([\w:]+)\s*;# ) {
166 1     1   37 no strict 'refs';
  1         4  
  1         460  
167 0         0 my $sub= $1 . '::PERSONA';
168 0 0       0 *{$sub}= \&main::PERSONA if !exists &$sub;
  0         0  
169             }
170              
171             # we'll do this line
172 0         0 $source .= $line;
173             }
174             # show source if *really* debugging
175 0         0 TELL $source if DEBUG > 2;
176              
177 0 0       0 return wantarray ? ( \$source, $skipped ) : \$source;
178             } #path2source
179              
180             #-------------------------------------------------------------------------------
181             #
182             # Standard Perl features
183             #
184             #-------------------------------------------------------------------------------
185             # import
186             #
187             # IN: 1 class (ignored)
188             # 2 .. N attributes
189              
190             sub import {
191 1     1   10 my ( undef, @attr )= @_;
192              
193             # assume we want to set persona if only one parameter
194 1 50       3 unshift @attr, 'persona' if @attr == 1;
195              
196             # fetch name's name
197 1   50     5 my $name= $ENV{ENV_PERSONA} || 'PERSONA';
198              
199             # fetch parameters we know
200 1         2 my @only_for_new;
201             my @huh;
202 1         8 while ( my ( $key, $value ) = splice @attr, 0, 2 ) {
203              
204             # setting module specification
205 0 0       0 if ( $key eq 'only_for' ) {
    0          
206 0         0 push @only_for_new, $value;
207             }
208              
209             # setting persona
210             elsif ( $key eq 'persona' ) {
211              
212             # huh?
213 0 0 0     0 die "Already have '$process_persona' as persona, "
214             . "cannot specify '$value' now"
215             if defined $process_persona and $value ne $process_persona;
216              
217 0         0 $ENV{$name}= $value;
218             }
219              
220             # don't know what to do with this
221             else {
222 0         0 push @huh, $key;
223             }
224             }
225              
226             # find persona we need to work for
227 1 50       4 if ( !defined $process_persona ) {
228 1         2 $process_persona= $ENV{$name};
229              
230             # too bad, we don't have a persona
231 1 50       2 $process_persona= '' if !defined $process_persona;
232              
233             # force some sanity
234 1 50       4 die "Persona may only contain alphanumeric characters,"
235             . " e.g. '$process_persona'"
236             if $process_persona =~ s#\W##sg;
237              
238             # create constant in main (for easy access later)
239 1 50       49 die $@ if !eval "sub main::PERSONA () { '$process_persona' }; 1";
240              
241             # we have a persona, great!
242 1 50       3 if ($process_persona) {
243              
244             # install handler if we have a persona
245 0         0 unshift @INC, \&_inc_handler;
246              
247             # we're being called in a script
248 0 0 0     0 if ( !( ()= caller(3) ) # only 3 levels below us
      0        
249             and ( (caller(0))[1] ne '-e' ) # not a one liner
250             and ( (caller(2))[3] eq '(eval)' # but an eval at lowest level
251             ) ) {
252              
253             # make sure we will process this file
254 0         0 unshift @only_for, qr#^$0$#; ## syn hilite
255              
256             # do the script, but through the @INC handler
257 0         0 TELL 'Recursively calling script for "%s"', $process_persona
258             if DEBUG;
259 0         0 do $0;
260              
261             # we're done, nothing left to do at this level
262 0         0 exit;
263             }
264              
265 0         0 TELL 'Interpreting source code as "%s"', $process_persona if DEBUG;
266             }
267             }
268              
269             # extra parameters
270 1 50       3 die "Don't know what to do with @{[ sort @huh ]}" if @huh;
  0         0  
271              
272             # we have some kind of specification which modules to check
273 1 50       3 if (@only_for_new) {
274              
275             # normalize all new settings
276             ONLY_FOR:
277 0         0 foreach my $only_for (@only_for_new) {
278              
279             # need to check what we have
280 0 0       0 if ( my $ref= ref $only_for ) {
    0          
281 0 0       0 die "Can only handle references of type '$ref'"
282             if $ref ne 'Regexp';
283            
284             # it's ok
285 0         0 push @only_for, $only_for;
286             }
287              
288             # do all and everything
289             elsif ( $only_for eq '*' ) {
290 0         0 $all= 1;
291 0         0 @only_for= @only_for_new= ();
292 0         0 TELL "Look for personas in all files" if DEBUG;
293 0         0 last ONLY_FOR;
294             }
295            
296             # just a string, make it a regexp
297             else {
298 0         0 push @only_for, qr#^$only_for#;
299             }
300             }
301              
302 0         0 TELL "Added regular expression for matching file names:\n %s",
303             join( "\n ", @only_for[ -@only_for_new .. $#only_for ] ),
304             if DEBUG and @only_for_new;
305             }
306              
307             # export constant to the caller if not done so already
308 1     1   16 no strict 'refs';
  1         2  
  1         416  
309 1         2 my $sub= caller() . '::PERSONA';
310 1 50       3 *{$sub}= \&main::PERSONA if !exists &$sub;
  0         0  
311              
312 1         8 return;
313             } #import
314              
315             #-------------------------------------------------------------------------------
316             #
317             # Internal subroutines
318             #
319             #-------------------------------------------------------------------------------
320             # _inc_handler
321             #
322             # IN: 1 code reference to this sub
323             # 2 file to look for
324             # OUT: 1 handle to read source from
325              
326             sub _inc_handler {
327 0     0     my ( $self, $file )= @_;
328              
329             # shouldn't handle this file, let require handle it (again)
330 0 0 0 0     if ( !$all and !first { $file =~ m#$_# } @only_for ) {
  0            
331 0           TELL 'Not handling %s', $file if DEBUG > 1;
332 0           return undef;
333             }
334              
335             # can't find ourselves?
336 0     0     my $first= first { $INC[$_] eq $self } 0 .. $#INC;
  0            
337 0 0         die "Could not find INC handler in @INC" if !defined $first;
338              
339             # could not find file, let require handle it (again)
340 0     0     my $path= first { -e } map { "$INC[$_]/$file" } $first + 1 .. $#INC;
  0            
  0            
341 0 0         if ( !$path ) {
342 0           TELL 'Could not find %s', $file if DEBUG > 1;
343 0           return undef;
344             }
345              
346             # parse the source
347 0           my ( $source, $skipped )= __PACKAGE__->path2source($path);
348              
349             # could not open file, or nothing skipped, let -require- handle it
350 0 0 0       return undef if !$source or !$skipped;
351              
352             # set %INC correctly
353 0           $path =~ s#^\./##; # normalize just as perl does
354 0           $INC{$file}=
355             "$path (skipped $skipped lines for persona '$process_persona')";
356              
357             # make sure that __FILE__ will be correct as well
358 0           $$source= "#line 1 $path\n$$source";
359              
360             # convert source to handle, so require can handle it
361 0 0         open( my $require, '<', $source )
362             or die "Could not open in-memory source for reading: $!";
363              
364 0           return $require;
365             } #_inc_handler
366              
367             #-------------------------------------------------------------------------------
368              
369             __END__