File Coverage

blib/lib/App/Rad.pm
Criterion Covered Total %
statement 26 293 8.8
branch 0 134 0.0
condition 0 23 0.0
subroutine 9 50 18.0
pod 28 33 84.8
total 63 533 11.8


line stmt bran cond sub pod time code
1             package App::Rad;
2 1     1   28 use 5.006;
  1         4  
  1         104  
3 1     1   5596 use App::Rad::Command;
  1         2  
  1         43  
4 1     1   3279 use App::Rad::Help;
  1         3  
  1         34  
5 1     1   7 use Carp ();
  1         2  
  1         18  
6 1     1   5 use warnings;
  1         2  
  1         24  
7 1     1   5 use strict;
  1         2  
  1         508  
8              
9             our $VERSION = '1.05'; # Experimental
10             {
11              
12             #========================#
13             # INTERNAL FUNCTIONS #
14             #========================#
15              
16             my @OPTIONS = ();
17              
18             # - "I'm so excited! Feels like I'm 14 again" (edenc on Rad)
19             sub _init {
20 0     0   0 my $c = shift;
21              
22             # instantiate references for the first time
23 0         0 $c->{'_ARGV'} = [];
24 0         0 $c->{'_options'} = {};
25 0         0 $c->{'_stash'} = {};
26 0         0 $c->{'_config'} = {};
27 0         0 $c->{'_plugins'} = [];
28              
29             # this internal variable holds
30             # references to all special
31             # pre-defined control functions
32 0         0 $c->{'_functions'} = {
33             'setup' => \&setup,
34             'pre_process' => \&pre_process,
35             'post_process' => \&post_process,
36             'default' => \&default,
37             'invalid' => \&invalid,
38             'teardown' => \&teardown,
39             };
40              
41             #load extensions
42 0         0 App::Rad::Help->load($c);
43 0         0 foreach (@OPTIONS) {
44 0 0       0 if ( $_ eq 'include' ) {
    0          
    0          
45 0         0 eval 'use App::Rad::Include; App::Rad::Include->load($c)';
46 0 0       0 Carp::croak 'error loading "include" extension.' if ($@);
47             }
48             elsif ( $_ eq 'exclude' ) {
49 0         0 eval 'use App::Rad::Exclude; App::Rad::Exclude->load($c)';
50 0 0       0 Carp::croak 'error loading "exclude" extension.' if ($@);
51             }
52             elsif ( $_ eq 'debug' ) {
53 0         0 $c->{'debug'} = 1;
54             }
55             else {
56 0         0 $c->load_plugin($_);
57             }
58             }
59              
60             # tiny cheat to avoid doing a lot of processing
61             # when not in debug mode. If needed, I'll create
62             # an actual is_debugging() method or something
63 0 0       0 if ( $c->{'debug'} ) {
64 0         0 $c->debug( 'initializing: default commands are: '
65             . join( ', ', $c->commands() ) );
66             }
67             }
68              
69             sub import {
70 1     1   3 my $class = shift;
71 1         72 @OPTIONS = @_;
72             }
73              
74             sub load_plugin {
75 0     0 1   my $c = shift;
76 0           my $plugin = shift;
77 0           my $class = ref $c;
78              
79 0           my $plugin_fullname = '';
80 0 0         if ( $plugin =~ s{^\+}{} ) {
81 0           $plugin_fullname = $plugin;
82             }
83             else {
84 0           $plugin_fullname = "App::Rad::Plugin::$plugin";
85             }
86 0           eval "use $plugin_fullname ()";
87 0 0         Carp::croak "error loading plugin '$plugin_fullname': $@\n"
88             if $@;
89 0           my %methods = _get_subs_from($plugin_fullname);
90              
91 0 0         Carp::croak "No methods found for plugin '$plugin_fullname'\n"
92             unless keys %methods > 0;
93              
94 1     1   6 no strict 'refs';
  1         8  
  1         409  
95 0           foreach my $method ( keys %methods ) {
96              
97             # don't add plugin's internal methods
98 0 0         next if substr( $method, 0, 1 ) eq '_';
99              
100 0           *{"$class\::$method"} = $methods{$method};
  0            
101 0           $c->debug("-- method '$method' added [$plugin_fullname]");
102              
103             # fill $c->plugins()
104 0           push @{ $c->{'_plugins'} }, $plugin;
  0            
105             }
106             }
107              
108             # this function browses a file's
109             # symbol table (usually 'main') and maps
110             # each function to a hash
111             #
112             # FIXME: if I create a sub here (Rad.pm) and
113             # there is a global variable with that same name
114             # inside the user's program (e.g.: sub ARGV {}),
115             # the name will appear here as a command. It really
116             # shouldn't...
117             sub _get_subs_from {
118 0   0 0     my $package = shift || 'main';
119 0           $package .= '::';
120              
121 0           my %subs = ();
122              
123 1     1   7 no strict 'refs';
  1         2  
  1         4948  
124 0           while ( my ( $key, $value ) = ( each %{ *{$package} } ) ) {
  0            
  0            
125 0           local (*SYMBOL) = $value;
126 0 0 0       if ( defined $value && defined *SYMBOL{CODE} ) {
127 0           $subs{$key} = *{$value}{CODE};
  0            
128             }
129             }
130 0           return %subs;
131             }
132              
133             # overrides our pre-defined control
134             # functions with any available
135             # user-defined ones
136             sub _register_functions {
137 0     0     my $c = shift;
138 0           my %subs = _get_subs_from('main');
139              
140             # replaces only if the function is
141             # in 'default', 'pre_process' or 'post_process'
142 0           foreach ( keys %{ $c->{'_functions'} } ) {
  0            
143 0 0         if ( defined $subs{$_} ) {
144 0           $c->debug("overriding $_ with user-defined function.");
145 0           $c->{'_functions'}->{$_} = $subs{$_};
146             }
147             }
148             }
149              
150             # retrieves command line arguments
151             # to be executed by the main program
152             sub parse_input {
153 0     0 0   my $c = shift;
154              
155             # parse global arguments out of ARGV
156 0 0         if ( $c->{'_globals'} ) {
157 0           $c->_parse( \@ARGV, $c->{'_globals'} );
158             }
159              
160             #TODO: this could use some major improvements
161             # now the next item in ARGV is our command name.
162             # If it doesn't exist, we make it blank so we
163             # can call the 'default' command
164 0           my $cmd = $c->{'cmd'} = '';
165 0 0         if ( defined $ARGV[0] ) {
166 0           my $cmd_obj = undef;
167              
168             # argument looks like command
169 0 0         if ( substr( $ARGV[0], 0, 1 ) ne '-' ) {
170 0           $cmd = shift @ARGV;
171 0           $c->{'cmd'} = $cmd;
172              
173             # valid command
174 0 0         if ( $c->is_command($cmd) ) {
175 0           $cmd_obj = $c->{'_commands'}->{$cmd};
176             }
177              
178             # invalid command
179             else {
180 0           $cmd = undef;
181             }
182             }
183 0           my @tARGV = @ARGV;
184 0           $c->_parse( \@tARGV, $cmd_obj );
185             }
186 0           return $cmd; # default (''), invalid (undef), command ($cmd)
187             }
188              
189             sub _parse {
190 0     0     my ( $c, $arg_ref, $cmd_obj ) = (@_);
191              
192             # al newkirk: conflict support
193 0           my @arg_names = ();
194 0           my @conflicts_with = ();
195              
196             # reset any previous value
197 0           %{ $c->options } = ();
  0            
198 0           @{ $c->argv } = ();
  0            
199              
200 0           while ( my $arg = shift @{$arg_ref} ) {
  0            
201              
202             # single option (could be grouped)
203 0 0         if ( $arg =~ m/^\-([^\-\=]+)$/o ) {
    0          
204 0           my @args = split //, $1;
205 0           foreach (@args) {
206              
207             # _parse_arg returns the options' name
208             # and its "to_stash" values as an arrayref,
209             # or undef and an error message.
210             # TODO: this is a horrible approach I took only
211             # because it's 4am and I'm in a rush to get it done.
212             # any attempts to rewrite the parser in order to
213             # improve it will be **much** appreciated. Thanks!
214 0           my ( $opt, $to_stash ) = ( $_, undef );
215 0 0         if ( defined $cmd_obj ) {
216 0           ( $opt, $to_stash ) = $cmd_obj->_parse_arg($opt);
217 0 0         unless ($opt) {
218 0           Carp::croak "Error: $to_stash";
219              
220             # TODO x 2: this should be forwared to an
221             # overridable help error handler or whatever
222             }
223             }
224              
225 0 0         $c->options->{$opt} =
226             ( defined $c->options->{$opt} )
227             ? $c->options->{$opt} + 1
228             : 1;
229              
230 0           foreach my $stash_key (@$to_stash) {
231 0 0         $c->stash->{$stash_key} =
232             ( defined $c->stash->{$stash_key} )
233             ? $c->stash->{$stash_key} + 1
234             : 1;
235             }
236             }
237             }
238              
239             # long option: --name or --name=value
240             elsif ( $arg =~ m/^\-\-([^\-\=]+)(?:\=(.+))?$/o ) {
241            
242 0 0         my ($key, $val) = ($1, (defined $2 ? $2 : ""));
243              
244             # al newkirk: when defaulting to a value of one, the type
245             # if exists, must be changed to "num" avoid attempting to validate "1"
246             # as "any" or "str" and failing.
247             # see - App::Rad::Command::_parse_arg
248              
249 0           my $to_stash = undef;
250              
251             # TODO: see above TODO :)
252 0 0         if ( defined $cmd_obj ) {
253              
254             # WARNING! al newkirk: I am adding an additional parameter
255             # to the cmd_obj which may break some other code.
256             # Hopefully not :)
257             # I am making App::Rad::Command aware of self ($c to be exact)
258 0           ( $key, $to_stash, $val ) = $cmd_obj->_parse_arg( $key, $val, $c );
259 0 0         if (!$key) {
260 0           Carp::croak "Error: $to_stash";
261             }
262             }
263              
264             # original code
265             # my ($key, $val) = ($1, (defined $2 ? $2 : 1));
266              
267             # al newkirk: my nasty little hacked in fail safe.
268             # added in default value checking before defaulting to ""
269            
270 0 0         unless ($to_stash) {
271 0 0 0       if (!$key || !$val) {
272 0 0         ( $key, $val ) = (
    0          
273             $1,
274             (
275             defined $2 ? $2
276             : (
277             defined $cmd_obj->{args}->{$1}->{default}
278             ? $cmd_obj->{args}->{$1}->{default}
279             : ""
280             )
281             )
282             );
283             }
284             }
285              
286 0           $c->options->{$key} = $val;
287 0           foreach my $stash_key (@$to_stash) {
288 0           $c->stash->{$stash_key} = $val;
289             }
290             # al newkirk: save key/name for conflict validation, etc
291 0 0         push ( @arg_names, $key ) if $key;
292            
293             # al newkirk: conflict support
294 0 0         push @conflicts_with, { arg => $key, conflict => $cmd_obj->{args}->{$key}->{conflicts_with} }
295             if defined $cmd_obj->{args}->{$key}->{conflicts_with};
296             }
297             else {
298 0           push @{ $c->argv }, $arg;
  0            
299             }
300             }
301             # al newkirk: conflict support
302             # Note! conflict support currently only works against args using the long option
303 0 0         if (@conflicts_with) {
304 0           foreach my $name (@arg_names) {
305 0 0         if ( grep { $name eq $_->{conflict} } @conflicts_with ) {
  0            
306 0           my @clist = map { $_->{arg} } @conflicts_with;
  0            
307 0           die "Error: $name conflicts with ". join(" and ", @clist ) ." and can not be use together.";
308             }
309             }
310             }
311             }
312              
313             sub _run_full_round {
314 0     0     my $c = shift;
315 0           my $sub = shift;
316              
317 0           $c->debug('calling pre_process function...');
318 0           $c->{'_functions'}->{'pre_process'}->($c);
319              
320 0           $c->debug('executing command...');
321 0           $c->{'output'} = $sub->($c);
322              
323 0           $c->debug('calling post_process function...');
324 0           $c->{'_functions'}->{'post_process'}->($c);
325              
326 0           $c->debug('reseting output');
327 0           $c->{'output'} = undef;
328             }
329              
330             #========================#
331             # PUBLIC METHODS #
332             #========================#
333              
334             sub load_config {
335 0     0 1   require App::Rad::Config;
336 0           App::Rad::Config::load_config(@_);
337             }
338              
339             sub path {
340 0     0 0   require FindBin;
341 0           return $FindBin::Bin;
342             }
343              
344             sub real_path {
345 0     0 0   require FindBin;
346 0           return $FindBin::RealBin;
347             }
348              
349             # - "Wow! you guys rock!" (zoso on Rad)
350             #TODO: this code probably could use some optimization
351             sub register_commands {
352 0     0 1   my $c = shift;
353 0           my %help_for_sub = ();
354 0           my %rules = ();
355              
356             # process parameters
357 0           foreach my $item (@_) {
358              
359             # if we receive a hash ref, it could be commands or
360             # rules for fetching commands.
361 0 0         if ( ref($item) ) {
362 0 0         Carp::croak
363             '"register_commands" may receive only HASH references'
364             unless ref $item eq 'HASH';
365              
366 0           foreach my $params ( keys %{$item} ) {
  0            
367 0 0 0       Carp::croak
368             'registered elements may only receive strings or hash references'
369             if ref $item->{$params}
370             and ref $item->{$params} ne 'HASH';
371              
372             # we got a rule - push it in.
373 0 0 0       if ( $params eq '-ignore_prefix'
      0        
374             or $params eq '-ignore_suffix'
375             or $params eq '-ignore_regexp' )
376             {
377 0           $rules{$params} = $item->{$params};
378             }
379              
380             # not a rule, so it's either a command with
381             # help text or a command with an argument list.
382             # either way, we push it to our 'help' hash.
383             else {
384 0           $help_for_sub{$params} = $item->{$params};
385             }
386             }
387             }
388             else {
389 0           $help_for_sub{$item} = undef; # no help text
390             }
391             }
392              
393             # hack, prevents registering methods from App::Rad namespace when
394             # using shell-mode - Al Newkirk (awnstudio)
395             # my $caller = ( caller(2) or 'main' );
396 0 0 0       my $caller =
397             (
398             caller(2) &&
399             caller(2) ne 'App::Rad' &&
400             caller(2) ne 'App::Rad::Shell'
401             ) ?
402             caller(2) : 'main';
403 0           my %subs = _get_subs_from($caller);
404              
405             # handles explicit command calls first, as
406             # they have priority over generic rules (below)
407 0           foreach my $cmd ( keys %help_for_sub ) {
408              
409             # we only add the sub to the commands
410             # list if it's *not* a control function
411 0 0         if ( not defined $c->{'_functions'}->{$cmd} ) {
412              
413 0 0         if ( $cmd eq '-globals' ) {
    0          
414              
415             # use may set it as a flag to enable global arguments
416             # or elaborate on each available argument
417 0     0     my %command_options = ( name => '', code => sub { } );
  0            
418 0 0         if ( ref $help_for_sub{$cmd} ) {
419 0           $command_options{args} = $help_for_sub{$cmd};
420             }
421 0           my $cmd_obj = App::Rad::Command->new( \%command_options );
422 0           $c->{'_globals'} = $cmd_obj;
423              
424             # $c->register(undef, undef, $help_for_sub{$cmd});
425             }
426              
427             # user wants to register a valid (existant) sub
428             elsif ( exists $subs{$cmd} ) {
429 0           $c->register( $cmd, $subs{$cmd}, $help_for_sub{$cmd} );
430             }
431             else {
432 0           Carp::croak
433             "'$cmd' does not appear to be a valid sub. Registering seems impossible.\n";
434             }
435             }
436             }
437              
438             # no parameters, or params+rules: try to register everything
439 0 0 0       if ( ( !%help_for_sub ) or %rules ) {
440 0           foreach my $subname ( keys %subs ) {
441              
442             # we only add the sub to the commands
443             # list if it's *not* a control function
444 0 0         if ( not defined $c->{'_functions'}->{$subname} ) {
445              
446 0 0         if ( $rules{'-ignore_prefix'} ) {
447             next
448             if (
449 0 0         substr(
450             $subname, 0,
451             length( $rules{'-ignore_prefix'} )
452             ) eq $rules{'-ignore_prefix'}
453             );
454             }
455 0 0         if ( $rules{'-ignore_suffix'} ) {
456             next
457             if (
458 0 0         substr(
459             $subname,
460             length($subname) -
461             length( $rules{'-ignore_suffix'} ),
462             length( $rules{'-ignore_suffix'} )
463             ) eq $rules{'-ignore_suffix'}
464             );
465             }
466 0 0         if ( $rules{'-ignore_regexp'} ) {
467 0           my $re = $rules{'-ignore_regexp'};
468 0 0         next if $subname =~ m/$re/o;
469             }
470              
471             # avoid duplicate registration
472 0 0         if ( !exists $help_for_sub{$subname} ) {
473 0           $c->register( $subname, $subs{$subname} );
474             }
475             }
476             }
477             }
478             }
479              
480 0     0 1   sub register_command { return register(@_) }
481              
482             sub register {
483 0     0 1   my ( $c, $command_name, $coderef, $extra ) = @_;
484              
485             # short circuit
486 0 0         return unless ref $coderef eq 'CODE';
487              
488 0           my %command_options = (
489             name => $command_name,
490             code => $coderef,
491             );
492              
493             # the extra parameter may be a help string
494             # or an argument hashref
495 0 0         if ($extra) {
496 0 0         if ( ref $extra ) {
497 0           $command_options{args} = $extra;
498             }
499             else {
500 0           $command_options{help} = $extra;
501             }
502             }
503              
504 0           my $cmd_obj = App::Rad::Command->new( \%command_options );
505 0 0         return unless $cmd_obj;
506              
507             #TODO: I don't think this message is ever being printed (wtf?)
508 0           $c->debug("registering $command_name as a command.");
509              
510 0           $c->{'_commands'}->{$command_name} = $cmd_obj;
511 0           return $command_name;
512             }
513              
514 0     0 1   sub unregister_command { return unregister(@_) }
515              
516             sub unregister {
517 0     0 1   my ( $c, $command_name ) = @_;
518              
519 0 0         if ( $c->{'_commands'}->{$command_name} ) {
520 0           delete $c->{'_commands'}->{$command_name};
521             }
522             else {
523 0           return undef;
524             }
525             }
526              
527             sub create_command_name {
528 0     0 1   my $id = 0;
529 0           foreach ( commands() ) {
530 0 0         if (m/^cmd(\d+)$/) {
531 0 0         $id = $1 if ( $1 > $id );
532             }
533             }
534 0           return 'cmd' . ( $id + 1 );
535             }
536              
537             sub commands {
538 0     0 1   return ( keys %{ $_[0]->{'_commands'} } );
  0            
539             }
540              
541             sub is_command {
542 0     0 1   my ( $c, $cmd ) = @_;
543             return (
544 0 0         defined $c->{'_commands'}->{$cmd}
545             ? 1
546             : 0
547             );
548             }
549              
550             sub command : lvalue {
551 0     0 1   cmd(@_);
552             }
553              
554             sub cmd : lvalue {
555 0     0 1   $_[0]->{'cmd'};
556             }
557              
558             # - "I'm loving having something else write up the 80% drudge
559             # code for the small things." (benh on Rad)
560             sub run {
561 0     0 1   my $class = shift;
562 0           my $c = {};
563 0           bless $c, $class;
564            
565             # set state
566 0           $c->{state} = 'cli';
567            
568 0           $c->_init();
569              
570             # first we update the control functions
571             # with any overriden value
572 0           $c->_register_functions();
573              
574             # then we run the setup to register
575             # some commands
576 0           $c->{'_functions'}->{'setup'}->($c);
577              
578             # now we get the actual input from
579             # the command line (someone using the app!)
580 0           my $cmd = $c->parse_input();
581              
582 0 0         if ( not defined $cmd ) {
    0          
583 0           $c->debug( "'"
584             . $c->cmd
585             . "' is not a valid command. Falling to invalid." );
586 0           $cmd = $c->{'_functions'}->{'invalid'};
587             }
588             elsif ( $cmd eq '' ) {
589 0           $c->debug('no command detected. Falling to default');
590 0           $cmd = $c->{'_functions'}->{'default'};
591             }
592             else {
593 0           my $obj = $c->{'_commands'}->{$cmd};
594              
595             # set default values for command (if available)
596 0           $obj->_set_default_values( $c->options, $c->stash );
597              
598 0     0     $cmd = sub { $obj->run(@_) }
599 0           }
600              
601             # run the specified command
602 0           $c->_run_full_round($cmd);
603              
604             # that's it. Tear down everything and go home :)
605 0           $c->{'_functions'}->{'teardown'}->($c);
606              
607 0           return 0;
608             }
609              
610             # run operations
611             # in a shell-like environment
612             sub shell {
613 0     0 0   my $class = shift;
614 0           my $params = shift;
615 0           require App::Rad::Shell;
616 0           return App::Rad::Shell::shell($class, $params);
617             }
618              
619             sub execute {
620 0     0 1   my ( $c, $cmd ) = @_;
621              
622             # given command has precedence
623 0 0         if ($cmd) {
624 0           $c->{'cmd'} = $cmd;
625             }
626             else {
627 0           $cmd = $c->{'cmd'}; # now $cmd always has the called cmd
628             }
629              
630             # valid command, run it and return the command name
631 0 0         if ( $c->is_command($cmd) ) {
632 0           my $cmd_obj = $c->{'_commands'}->{$cmd};
633              
634             # set default values for command (if available)
635 0           $cmd_obj->_set_default_values( $c->options, $c->stash );
636              
637 0     0     $c->_run_full_round( sub { $cmd_obj->run(@_) } );
  0            
638 0           return $cmd;
639             }
640             else {
641              
642             # if not a command, return undef
643 0           return;
644             }
645             }
646              
647 0     0 1   sub argv { return $_[0]->{'_ARGV'} }
648 0     0 1   sub options { return $_[0]->{'_options'} }
649 0     0 1   sub stash { return $_[0]->{'_stash'} }
650 0     0 1   sub config { return $_[0]->{'_config'} }
651            
652             # get user information via prompting - Al Newkirk (awnstudio)
653 0     0 0   sub prompt { return App::Rad::Shell::prompt(@_); }
654              
655             # $c->plugins is sort of "read-only" externally
656             sub plugins {
657 0     0 1   my @plugins = @{ $_[0]->{'_plugins'} };
  0            
658 0           return @plugins;
659             }
660              
661             sub getopt {
662 0     0 1   require Getopt::Long;
663 0 0         Carp::croak "Getopt::Long needs to be version 2.36 or above"
664             unless $Getopt::Long::VERSION >= 2.36;
665              
666 0           my ( $c, @options ) = @_;
667              
668             # reset values from tinygetopt
669             #$c->{'_options'} = {};
670 0           %{ $c->options } = ();
  0            
671              
672 0           my $parser = new Getopt::Long::Parser;
673 0           $parser->configure(qw(bundling));
674              
675 0           my @tARGV = @ARGV; # we gotta stick to our API
676 0           my $ret = $parser->getoptions( $c->{'_options'}, @options );
677 0           @{ $c->argv } = @ARGV;
  0            
678 0           @ARGV = @tARGV;
679              
680 0           return $ret;
681             }
682              
683             sub debug {
684 0 0   0 1   if ( shift->{'debug'} ) {
685 0           print "[debug] @_\n";
686             }
687             }
688              
689             # gets/sets the output (returned value)
690             # of a command, to be post processed
691             sub output {
692 0     0 1   my ( $c, @msg ) = @_;
693 0 0         if (@msg) {
694 0           $c->{'output'} = join( ' ', @msg );
695             }
696             else {
697 0           return $c->{'output'};
698             }
699             }
700              
701             #=========================#
702             # CONTROL FUNCTIONS #
703             #=========================#
704              
705 0     0 1   sub setup { $_[0]->register_commands( { -ignore_prefix => '_' } ) }
706              
707 0     0 1   sub teardown { }
708              
709 0     0 1   sub pre_process { }
710              
711             sub post_process {
712 0     0 1   my $c = shift;
713              
714 0 0         if ( $c->output() ) {
715 0           print $c->output() . $/;
716             }
717             }
718              
719             sub default {
720 0     0 1   my $c = shift;
721 0           return $c->{'_commands'}->{'help'}->run($c);
722             }
723              
724             sub invalid {
725 0     0 1   my $c = shift;
726 0           return $c->{'_functions'}->{'default'}->($c);
727             }
728            
729             #sub error {
730             # my $c = shift;
731             # my $e = shift;
732             #
733             # if ( $c->{state} eq "shell" ) {
734             # # should probably return
735             # return print "$e\n";
736             # }
737             # else {
738             # Carp::croak "$e\n";
739             # }
740             #}
741              
742             }
743             42; # ...and thus ends thy module ;)
744             __END__