File Coverage

blib/lib/Applify.pm
Criterion Covered Total %
statement 213 232 91.8
branch 97 122 79.5
condition 17 27 62.9
subroutine 37 38 97.3
pod 9 9 100.0
total 373 428 87.1


line stmt bran cond sub pod time code
1             package Applify;
2              
3             =head1 NAME
4              
5             Applify - Write object oriented scripts with ease
6              
7             =head1 VERSION
8              
9             0.12
10              
11             =head1 DESCRIPTION
12              
13             This module should keep all the noise away and let you write scripts
14             very easily. These scripts can even be unittested even though they
15             are define directly in the script file and not in a module.
16              
17             =head1 SYNOPSIS
18              
19             #!/usr/bin/perl
20             use Applify;
21              
22             option file => input_file => 'File to read from';
23             option dir => output_dir => 'Directory to write files to';
24             option flag => dry_run => 'Use --no-dry-run to actually do something', 1;
25              
26             documentation __FILE__;
27             version 1.23;
28              
29             sub generate_exit_value {
30             return int rand 100;
31             }
32              
33             app {
34             my($self, @extra) = @_;
35             my $exit_value = 0;
36              
37             print "Extra arguments: @extra\n" if(@extra);
38             print "Will read from: ", $self->input_file, "\n";
39             print "Will write files to: ", $self->output_dir, "\n";
40              
41             if($self->dry_run) {
42             die 'Will not run script';
43             }
44              
45             return $self->generate_exit_value;
46             };
47              
48             =head1 APPLICATION CLASS
49              
50             This module will generate an application class, which C<$self> inside the
51             L block refere to. This class will have:
52              
53             =over 4
54              
55             =item * new()
56              
57             An object constructor. This method will not be auto generated if any of
58             the classes given to L has the method C.
59              
60             =item * run()
61              
62             This method is basically the code block given to L.
63              
64             =item * Other methods
65              
66             Other methods defined in the script file will be accesible from C<$self>
67             inside C.
68              
69             =item * _script()
70              
71             This is an accessor which return the L object which
72             is refered to as C<$self> in this documentation.
73              
74             NOTE: This accessor starts with an underscore to prevent conflicts
75             with L.
76              
77             =item * Other accessors
78              
79             Any L (application switch) will be available as an accessor on the
80             application object.
81              
82             =back
83              
84             =cut
85              
86 10     10   24501 use strict;
  10         16  
  10         325  
87 9     9   52 use warnings;
  9         12  
  9         310  
88 9     9   42 use File::Basename ();
  9         11  
  9         385  
89              
90 9 50   9   543 use constant SUB_NAME_IS_AVAILABLE => $INC{'App/FatPacker/Trace.pm'}
  9 50       3302  
  0         0  
  0         0  
91             ? 0 # this will be true when running under "fatpack"
92 9     9   42 : eval 'use Sub::Name; 1' ? 1 : 0;
  9         13  
93              
94             our $VERSION = '0.12';
95             our $PERLDOC = 'perldoc';
96             my $ANON = 1;
97              
98             sub __new_sub {
99 42     42   49 my ($fqn, $code) = @_;
100 9     9   49 no strict 'refs';
  9         11  
  9         10952  
101 42 50       183 return if *$fqn{CODE};
102 42         100 *$fqn = SUB_NAME_IS_AVAILABLE ? Sub::Name::subname($fqn, $code) : $code;
103             }
104              
105             =head1 EXPORTED FUNCTIONS
106              
107             =head2 option
108              
109             option $type => $name => $documentation;
110             option $type => $name => $documentation, $default;
111             option $type => $name => $documentation, $default, @args;
112             option $type => $name => $documentation, @args;
113              
114             This function is used to define options which can be given to this
115             application. See L for example code. This function can also be
116             called as a method on C<$self>.
117              
118             =over 4
119              
120             =item * $type
121              
122             Used to define value types for this input.
123              
124             =over 4
125              
126             =item bool, flag
127              
128             =item inc
129              
130             =item str
131              
132             =item int
133              
134             =item num
135              
136             =item file (TODO)
137              
138             =item dir (TODO)
139              
140             =back
141              
142             =item * $name
143              
144             The name of an application switch. This name will also be used as
145             accessor name inside the application. Example:
146              
147             # define an application switch:
148             option file => some_file => '...';
149              
150             # call the application from command line:
151             > myapp.pl --some-file /foo/bar
152              
153             # run the application code:
154             app {
155             my $self = shift;
156             print $self->some_file # prints "/foo/bar"
157             return 0;
158             };
159              
160             =item * C<$documentation>
161              
162             Used as description text when printing the usage text.
163              
164             =item * C<@args>
165              
166             =over 4
167              
168             =item * C
169              
170             The script will not start if a required field is omitted.
171              
172             =item * C
173              
174             Allow the option to hold a list of values. Examples: "@", "4", "1,3".
175             See L for details.
176              
177             =item * Other
178              
179             Any other L attribute argument may/will be supported in
180             future release.
181              
182             =back
183              
184             =back
185              
186             =cut
187              
188             sub option {
189 16     16 1 3929 my $self = shift;
190 16 100       56 my $type = shift or die 'Usage: option $type => ...';
191 15 100       40 my $name = shift or die 'Usage: option $type => $name => ...';
192 14 100       38 my $documentation = shift or die 'Usage: option $type => $name => $documentation, ...';
193 13         32 my ($default, %args);
194              
195 13 100       34 if (@_ % 2) {
196 5         9 $default = shift;
197 5         12 %args = @_;
198             }
199             else {
200 8         19 %args = @_;
201             }
202              
203 13 100 66     42 if ($args{alias} and !ref $args{alias}) {
204 1         2 $args{alias} = [$args{alias}];
205             }
206              
207 13         10 push @{$self->{options}}, {default => $default, %args, type => $type, name => $name, documentation => $documentation};
  13         64  
208              
209 13         61 return $self;
210             }
211              
212             =head2 documentation
213              
214             documentation __FILE__; # current file
215             documentation '/path/to/file';
216             documentation 'Some::Module';
217              
218             Specifies where to retrieve documentaion from when giving the C<--man>
219             switch to your script.
220              
221             =cut
222              
223             sub documentation {
224 24 100   24 1 1105 return $_[0]->{documentation} if @_ == 1;
225 4 100       21 $_[0]->{documentation} = $_[1] or die 'Usage: documentation $file|$module_name;';
226 3         8 return $_[0];
227             }
228              
229             =head2 version
230              
231             version 'Some::Module';
232             version $num;
233              
234             Specifies where to retrieve the version number from when giving the
235             C<--version> switch to your script.
236              
237             =cut
238              
239             sub version {
240 25 100   25 1 1703 return $_[0]->{version} if @_ == 1;
241 5 100       23 $_[0]->{version} = $_[1] or die 'Usage: version $module_name|$num;';
242 4         10 return $_[0];
243             }
244              
245             =head2 extends
246              
247             extends @classes;
248              
249             Specify which classes this application should inherit from. These
250             classes can be L based.
251              
252             =cut
253              
254             sub extends {
255 1     1 1 2 my $self = shift;
256 1         2 $self->{extends} = [@_];
257 1         8 return $self;
258             }
259              
260             =head2 app
261              
262             app CODE;
263              
264             This function will define the code block which is called when the application
265             is started. See L for example code. This function can also be
266             called as a method on C<$self>.
267              
268             IMPORTANT: This function must be the last function called in the script file
269             for unittests to work. Reason for this is that this function runs the
270             application in void context (started from command line), but returns the
271             application object in list/scalar context (from L).
272              
273             =cut
274              
275             sub app {
276 11     11 1 45 my $self = shift;
277 11   66     74 my $code = $self->{app} ||= shift;
278 11         35 my $parser = $self->_option_parser;
279 11         806 my (%options, @options_spec, $application_class, $app);
280              
281 11         19 for my $option (@{$self->{options}}) {
  11         44  
282 13         36 my $switch = $self->_attr_to_option($option->{name});
283 13         30 push @options_spec, $self->_calculate_option_spec($option);
284 13 50       47 $options{$switch} = $option->{default} if exists $option->{default};
285             }
286              
287 11 50       49 unless ($parser->getoptions(\%options, @options_spec, $self->_default_options)) {
288 0         0 $self->_exit(1);
289             }
290              
291 11 50       2980 if ($options{help}) {
    50          
    50          
292 0         0 $self->print_help;
293 0         0 $self->_exit('help');
294             }
295             elsif ($options{man}) {
296 0         0 system $PERLDOC => $self->documentation;
297 0         0 $self->_exit($? >> 8);
298             }
299             elsif ($options{version}) {
300 0         0 $self->print_version;
301 0         0 $self->_exit('version');
302             }
303              
304 11   66     119 $application_class = $self->{application_class} ||= $self->_generate_application_class($code);
305 11         49 $app = $application_class->new({map { my $k = $self->_option_to_attr($_); $k => $options{$_} } keys %options});
  13         24  
  13         45  
306              
307 11 50       154 return $app if defined wantarray; # $app = do $script_file;
308 0         0 $self->_exit($app->run(@ARGV));
309             }
310              
311             sub _calculate_option_spec {
312 23     23   35 my ($self, $option) = @_;
313 23         50 my $spec = $self->_attr_to_option($option->{name});
314              
315 23 100       59 if (ref $option->{alias} eq 'ARRAY') {
316 2         4 $spec .= join '|', '', @{$option->{alias}};
  2         7  
317             }
318              
319 23 100       200 if ($option->{type} =~ /^(?:bool|flag)/i) { $spec .= '!' }
  3 100       6  
    100          
    100          
    100          
    100          
    50          
320 1         2 elsif ($option->{type} =~ /^inc/) { $spec .= '+' }
321 12         19 elsif ($option->{type} =~ /^str/) { $spec .= '=s' }
322 1         3 elsif ($option->{type} =~ /^int/i) { $spec .= '=i' }
323 3         6 elsif ($option->{type} =~ /^num/i) { $spec .= '=f' }
324 2         5 elsif ($option->{type} =~ /^file/) { $spec .= '=s' } # TODO
325 1         4 elsif ($option->{type} =~ /^dir/) { $spec .= '=s' } # TODO
326 0         0 else { die 'Usage: option {bool|flag|inc|str|int|num|file|dir} ...' }
327              
328 23 100       56 if (my $n_of = $option->{n_of}) {
329 3 100       14 $spec .= $n_of eq '@' ? $n_of : "{$n_of}";
330             $option->{default}
331 3 50 33     15 and ref $option->{default} ne 'ARRAY'
332             and die 'Usage option ... default => [Need to be an array ref]';
333 3   50     20 $option->{default} ||= [];
334             }
335              
336 23         74 return $spec;
337             }
338              
339             sub _default_options {
340 13     13   1449 my $self = shift;
341 13         22 my @default;
342              
343 13         55 push @default, 'help';
344 13 100       45 push @default, 'man' if $self->documentation;
345 13 100       42 push @default, 'version' if $self->version;
346              
347 13         96 return @default;
348             }
349              
350             sub _generate_application_class {
351 9     9   25 my ($self, $code) = @_;
352 9         24 my $application_class = $self->{caller}[1];
353 9   100     52 my $extends = $self->{extends} || [];
354 9         13 my ($meta, @required);
355              
356 9         75 $application_class =~ s!\W!_!g;
357 9         49 $application_class = join '::', ref($self), "__ANON__${ANON}__", $application_class;
358 9         17 $ANON++;
359              
360 8 50   8   59 eval qq[
  8         15  
  8         486  
  9         854  
361             package $application_class;
362             use base qw/ @$extends /;
363             1;
364             ] or die "Failed to generate applicatin class: $@";
365              
366             {
367 9     9   55 no strict 'refs';
  9         13  
  9         13162  
  9         17  
368 11     11   20 __new_sub "$application_class\::new" => sub { my $class = shift; bless shift, $class }
  11         28  
369 9 50       93 unless grep { $_->can('new') } @$extends;
  1         16  
370 9     7   50 __new_sub "$application_class\::_script" => sub {$self};
  7         53  
371             __new_sub "$application_class\::run" => sub {
372 1     1   2178 my ($app, @extra) = @_;
373              
374 1 50       2 if (@required = grep { not defined $app->{$_} } @required) {
  1         9  
375 1         2 my $required = join ', ', map { '--' . $self->_attr_to_option($_) } @required;
  1         3  
376 1         4 $app->_script->print_help;
377 1         6 die "Required attribute missing: $required\n";
378             }
379              
380 0         0 return $app->$code(@extra);
381 9         57 };
382              
383 9         27 for ('app', $self->{caller}[0]) {
384 18         24 my $ns = \%{"$_\::"};
  18         58  
385              
386 18         581 for my $name (keys %$ns) {
387 2302 100       3575 $self->{skip_subs}{$name} and next;
388 85 100       77 my $code = eval { ref $ns->{$name} eq 'CODE' ? $ns->{$name} : *{$ns->{$name}}{CODE} } or next;
  85 100       140  
  82         292  
389 5         15 my $fqn = join '::', $application_class, $name;
390 5         11 __new_sub $fqn => $code;
391 5         17 delete $ns->{$name}; # may be a bit too destructive?
392             }
393             }
394              
395 9 50 33     140 $meta = $application_class->meta if $application_class->isa('Moose::Object') and $application_class->can('meta');
396              
397 9         18 for my $option (@{$self->{options}}) {
  9         33  
398 10         16 my $name = $option->{name};
399 10         17 my $fqn = join '::', $application_class, $name;
400 10 50       16 if ($meta) {
401 0         0 $meta->add_attribute($name => {is => 'rw', default => $option->{default}});
402             }
403             else {
404 10 100   9   40 __new_sub $fqn => sub { @_ == 2 and $_[0]->{$name} = $_[1]; $_[0]->{$name} };
  9         534  
  9         54  
405             }
406 10 100       25 push @required, $name if $option->{required};
407             }
408             }
409              
410 9         43 return $application_class;
411             }
412              
413             =head1 ATTRIBUTES
414              
415             =head2 options
416              
417             $array_ref = $self->options;
418              
419             Holds the application options given to L.
420              
421             =cut
422              
423 4     4 1 30 sub options { $_[0]->{options} }
424              
425             sub _option_parser {
426 13   66 13   74 $_[0]->{_option_parser} ||= do {
427 8         7277 require Getopt::Long;
428 8         86771 Getopt::Long::Parser->new(config => [qw( no_auto_help no_auto_version pass_through )]);
429             };
430             }
431              
432             =head1 METHODS
433              
434             =head2 new
435              
436             $self = $class->new({ options => $array_ref, ... });
437              
438             Object constructor. Creates a new object representing the script meta
439             information.
440              
441             =cut
442              
443             sub new {
444 8     8 1 14 my ($class, $args) = @_;
445 8         17 my $self = bless $args, $class;
446              
447 8   50     100 $self->{options} ||= [];
448 8 50       27 $self->{caller} or die 'Usage: $self->new({ caller => [...], ... })';
449              
450 8         20 return $self;
451             }
452              
453             =head2 print_help
454              
455             Will print L to selected filehandle (STDOUT by default) in
456             a normalized matter. Example:
457              
458             Usage:
459             --foo Foo does this and that
460             * --bar Bar does something else
461              
462             --help Print this help text
463             --man Display manual for this application
464             --version Print application name and version
465              
466             =cut
467              
468             sub print_help {
469 3     3 1 1034 my $self = shift;
470 3         4 my @options = @{$self->{options}};
  3         37  
471 3         6 my $width = 0;
472              
473 3         11 push @options, {name => ''};
474 3         11 push @options, {name => 'help', documentation => 'Print this help text'};
475 3 100       10 push @options, {name => 'man', documentation => 'Display manual for this application'} if $self->documentation;
476 3 100       8 push @options, {name => 'version', documentation => 'Print application name and version'} if $self->version;
477 3         9 push @options, {name => ''};
478              
479 3         11 $self->_print_synopsis;
480              
481             OPTION:
482 3         8 for my $option (@options) {
483 24         25 my $length = length $option->{name};
484 24 100       42 $width = $length if $width < $length;
485             }
486              
487 3         8 print "Usage:\n";
488              
489             OPTION:
490 3         6 for my $option (@options) {
491 24 100       36 my $name = $self->_attr_to_option($option->{name}) or do { print "\n"; next OPTION };
  6         7  
  6         10  
492              
493             printf(
494             " %s %2s%-${width}s %s\n",
495             $option->{required} ? '*' : ' ',
496             length($name) > 1 ? '--' : '-',
497             $name, $option->{documentation},
498 18 100       108 );
    100          
499             }
500              
501 3         14 return $self;
502             }
503              
504             sub _print_synopsis {
505 3     3   4 my $self = shift;
506 3 100       7 my $documentation = $self->documentation or return;
507 2         3 my $print;
508              
509 2 50       48 unless (-e $documentation) {
510 0 0       0 eval "use $documentation; 1" or die "Could not load $documentation: $@";
511 0         0 $documentation =~ s!::!/!g;
512 0         0 $documentation = $INC{"$documentation.pm"};
513             }
514              
515 2 50       68 open my $FH, '<', $documentation or die "Failed to read synopsis from $documentation: $@";
516              
517 2         54 while (<$FH>) {
518 98 100 100     204 last if $print and /^=(?:cut|head1)/;
519 96 100       125 print if $print;
520 96 100       232 $print = 1 if /^=head1 SYNOPSIS/;
521             }
522             }
523              
524             =head2 print_version
525              
526             Will print L to selected filehandle (STDOUT by default) in
527             a normalized matter. Example:
528              
529             some-script.pl version 1.23
530              
531             =cut
532              
533             sub print_version {
534 3     3 1 933 my $self = shift;
535 3 100       7 my $version = $self->version or die 'Cannot print version without version()';
536              
537 2 100       10 unless ($version =~ m!^\d!) {
538 1 50       60 eval "require $version; 1" or die "Could not load $version: $@";
539 1         15 $version = $version->VERSION;
540             }
541              
542 2         81 printf "%s version %s\n", File::Basename::basename($0), $version;
543             }
544              
545             sub _exit {
546 0     0   0 my ($self, $reason) = @_;
547 0 0       0 exit 0 unless ($reason =~ /^\d+$/); # may change without warning...
548 0         0 exit $reason;
549             }
550              
551             sub _attr_to_option {
552 61 100   61   141 local $_ = $_[1] or return;
553 55         111 s!_!-!g;
554 55         111 $_;
555             }
556              
557             sub _option_to_attr {
558 13 50   13   30 local $_ = $_[1] or return;
559 13         28 s!-!_!g;
560 13         21 $_;
561             }
562              
563             =head2 import
564              
565             Will export the functions listed under L. The functions
566             will act on a L object created by this method.
567              
568             =cut
569              
570             sub import {
571 8     8   49 my $class = shift;
572 8         30 my @caller = caller;
573 8         32 my $self = $class->new({caller => \@caller});
574 8         23 my $ns = $caller[0] . '::';
575              
576 8         44 strict->import;
577 8         88 warnings->import;
578              
579 8         29 $self->{skip_subs} = {app => 1, option => 1, version => 1, documentation => 1, extends => 1,};
580              
581 9     9   55 no strict 'refs';
  9         22  
  9         552  
582 8         652 for my $name (keys %$ns) {
583 1929         2434 $self->{'skip_subs'}{$name} = 1;
584             }
585              
586 9     9   45 no warnings 'redefine'; # need to allow redefine when loading a new app
  9         13  
  9         2380  
587 8     8   127 *{"$caller[0]\::app"} = sub (&) { $self->app(@_) };
  8         46  
  8         188  
588 8     7   25 *{"$caller[0]\::option"} = sub { $self->option(@_) };
  8         30  
  7         20  
589 8     1   19 *{"$caller[0]\::version"} = sub { $self->version(@_) };
  8         27  
  1         3  
590 8     1   21 *{"$caller[0]\::documentation"} = sub { $self->documentation(@_) };
  8         28  
  1         1  
591 8     1   22 *{"$caller[0]\::extends"} = sub { $self->extends(@_) };
  8         681  
  1         5  
592             }
593              
594             =head1 COPYRIGHT & LICENSE
595              
596             This library is free software. You can redistribute it and/or modify
597             it under the same terms as Perl itself.
598              
599             =head1 AUTHOR
600              
601             Jan Henning Thorsen
602              
603             =cut
604              
605             1;