File Coverage

blib/lib/App/Rad.pm
Criterion Covered Total %
statement 3 5 60.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 5 7 71.4


line stmt bran cond sub pod time code
1             package App::Rad;
2 4     4   92038 use 5.006;
  4         17  
3 4     4   4232 use App::Rad::Parser;
  0            
  0            
4             use App::Rad::Command;
5             use App::Rad::Help;
6             use Carp ();
7             use warnings;
8             use strict;
9              
10             our $VERSION = '1.05_01';
11             {
12              
13             #========================#
14             # INTERNAL FUNCTIONS #
15             #========================#
16              
17             my @OPTIONS = ();
18              
19             # - "I'm so excited! Feels like I'm 14 again" (edenc on Rad)
20             sub _init {
21             my $c = shift;
22              
23             # instantiate references for the first time
24             $c->{'_ARGV'} = [];
25             $c->{'_stash'} = {};
26             $c->{'_config'} = {};
27             $c->{'_plugins'} = [];
28              
29             # this internal variable holds
30             # references to all special
31             # pre-defined control functions
32             $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             # create our standard global command
42             $c->register( '', sub {} );
43              
44             #load extensions
45             App::Rad::Help->load($c);
46             foreach (@OPTIONS) {
47             if ( $_ eq 'include' ) {
48             eval 'use App::Rad::Include; App::Rad::Include->load($c)';
49             Carp::croak 'error loading "include" extension.' if ($@);
50             }
51             elsif ( $_ eq 'exclude' ) {
52             eval 'use App::Rad::Exclude; App::Rad::Exclude->load($c)';
53             Carp::croak 'error loading "exclude" extension.' if ($@);
54             }
55             elsif ( $_ eq 'debug' ) {
56             $c->{'debug'} = 1;
57             }
58             else {
59             $c->load_plugin($_);
60             }
61             }
62              
63             # tiny cheat to avoid doing a lot of processing
64             # when not in debug mode. If needed, I'll create
65             # an actual is_debugging() method or something
66             if ( $c->{'debug'} ) {
67             $c->debug( 'initializing: default commands are: '
68             . join( ', ', $c->commands() ) );
69             }
70             }
71              
72             sub import {
73             my $class = shift;
74             @OPTIONS = @_;
75             }
76              
77             sub load_plugin {
78             my $c = shift;
79             my $plugin = shift;
80             my $class = ref $c;
81              
82             my $plugin_fullname = '';
83             if ( $plugin =~ s{^\+}{} ) {
84             $plugin_fullname = $plugin;
85             }
86             else {
87             $plugin_fullname = "App::Rad::Plugin::$plugin";
88             }
89             eval "use $plugin_fullname ()";
90             Carp::croak "error loading plugin '$plugin_fullname': $@\n"
91             if $@;
92             my %methods = _get_subs_from($plugin_fullname);
93              
94             Carp::croak "No methods found for plugin '$plugin_fullname'\n"
95             unless keys %methods > 0;
96              
97             no strict 'refs';
98             foreach my $method ( keys %methods ) {
99              
100             # don't add plugin's internal methods
101             next if substr( $method, 0, 1 ) eq '_';
102              
103             *{"$class\::$method"} = $methods{$method};
104             $c->debug("-- method '$method' added [$plugin_fullname]");
105             }
106             # add plugin to $c->plugins() list
107             push @{ $c->{'_plugins'} }, $plugin;
108             }
109              
110             # this function browses a file's
111             # symbol table (usually 'main') and maps
112             # each function to a hash
113             #
114             # FIXME: if I create a sub here (Rad.pm) and
115             # there is a global variable with that same name
116             # inside the user's program (e.g.: sub ARGV {}),
117             # the name will appear here as a command. It really
118             # shouldn't...
119             sub _get_subs_from {
120             my $package = shift || 'main';
121             $package .= '::';
122              
123             my %subs = ();
124              
125             no strict 'refs';
126             while ( my ( $key, $value ) = ( each %{ *{$package} } ) ) {
127             local (*SYMBOL) = $value;
128             if ( defined $value && defined *SYMBOL{CODE} ) {
129             $subs{$key} = *{$value}{CODE};
130             }
131             }
132             return %subs;
133             }
134              
135             # overrides our pre-defined control
136             # functions with any available
137             # user-defined ones
138             sub _register_functions {
139             my $c = shift;
140             my %subs = _get_subs_from('main');
141              
142             # replaces only if the function is
143             # in 'default', 'pre_process' or 'post_process'
144             foreach ( keys %{ $c->{'_functions'} } ) {
145             if ( defined $subs{$_} ) {
146             $c->debug("overriding $_ with user-defined function.");
147             $c->{'_functions'}->{$_} = $subs{$_};
148             }
149             }
150             }
151              
152             sub _run_full_round {
153             my $c = shift;
154              
155             $c->debug('calling pre_process function...');
156             $c->{'_functions'}->{'pre_process'}->($c);
157              
158             my $cmd_obj = $c->{'_commands'}->{ $c->cmd };
159              
160             $c->debug('executing command...');
161             $c->{'output'} = $cmd_obj->run($c, @_);
162              
163             $c->debug('calling post_process function...');
164             $c->{'_functions'}->{'post_process'}->($c);
165              
166             $c->debug('reseting output');
167             $c->{'output'} = undef;
168             }
169              
170             #========================#
171             # PUBLIC METHODS #
172             #========================#
173              
174             sub load_config {
175             require App::Rad::Config;
176             App::Rad::Config::load_config(@_);
177             }
178              
179             #TODO save_config
180              
181             sub path {
182             require FindBin;
183             return $FindBin::Bin;
184             }
185              
186             sub real_path {
187             require FindBin;
188             return $FindBin::RealBin;
189             }
190              
191             # - "Wow! you guys rock!" (zoso on Rad)
192             #TODO: this code probably could use some optimization
193             sub register_commands {
194             my $c = shift;
195             my %help_for_sub = ();
196             my %rules = ();
197              
198             # process parameters
199             foreach my $item (@_) {
200              
201             # if we receive a hash ref, it could be commands or
202             # rules for fetching commands.
203             if ( ref($item) ) {
204             Carp::croak '"register_commands" may receive only HASH references'
205             unless ref $item eq 'HASH';
206              
207             foreach my $params ( keys %{$item} ) {
208             Carp::croak 'registered elements may only receive strings or hash references'
209             if ref $item->{$params} and ref $item->{$params} ne 'HASH';
210              
211             # we got a rule - push it in.
212             if ( $params eq '-ignore_prefix'
213             or $params eq '-ignore_suffix'
214             or $params eq '-ignore_regexp'
215             ) {
216             $rules{$params} = $item->{$params};
217             }
218              
219             # not a rule, so it's either a command with
220             # help text or a command with an argument list.
221             # either way, we push it to our 'help' hash.
222             else {
223             $help_for_sub{$params} = $item->{$params};
224             }
225             }
226             }
227             else {
228             $help_for_sub{$item} = undef; # no help text
229             }
230             }
231              
232             # hack, prevents registering methods from App::Rad namespace when
233             # using shell-mode - Al Newkirk (awnstudio)
234             # my $caller = ( caller(2) or 'main' );
235             my $caller =
236             (
237             caller(2) &&
238             caller(2) ne 'App::Rad' &&
239             caller(2) ne 'App::Rad::Shell'
240             ) ?
241             caller(2) : 'main';
242             my %subs = _get_subs_from($caller);
243              
244             # handles explicit command calls first, as
245             # they have priority over generic rules (below)
246             foreach my $cmd ( keys %help_for_sub ) {
247              
248             # we only add the sub to the commands
249             # list if it's *not* a control function
250             if ( not defined $c->{'_functions'}->{$cmd} ) {
251              
252             if ( $cmd eq '-globals' ) {
253             # use may set it as a flag to enable global arguments
254             # or elaborate on each available argument
255             # globals is a command named ''
256             $c->register( '', sub {} );
257             # TODO: help showing 'Global options:'
258             }
259              
260             # user wants to register a valid (existant) sub
261             elsif ( exists $subs{$cmd} ) {
262             $c->register( $cmd, $subs{$cmd}, $help_for_sub{$cmd} );
263             }
264             else {
265             Carp::croak "'$cmd' does not appear to be a valid sub. Registering seems impossible.\n";
266             }
267             }
268             }
269              
270             # no parameters, or params+rules: try to register everything
271             if ( ( !%help_for_sub ) or %rules ) {
272             foreach my $subname ( keys %subs ) {
273             # we only add the sub to the commands
274             # list if it's *not* a control function
275             if ( not defined $c->{'_functions'}->{$subname} ) {
276             if ( $rules{'-ignore_prefix'} ) {
277             next if substr( $subname, 0, length( $rules{'-ignore_prefix'} ) )
278             eq $rules{'-ignore_prefix'};
279             }
280             if ( $rules{'-ignore_suffix'} ) {
281             next if substr( $subname,
282             length($subname) - length( $rules{'-ignore_suffix'} ),
283             length( $rules{'-ignore_suffix'} )
284             ) eq $rules{'-ignore_suffix'};
285             }
286             if ( $rules{'-ignore_regexp'} ) {
287             my $re = $rules{'-ignore_regexp'};
288             next if $subname =~ m/$re/o;
289             }
290              
291             # avoid duplicate registration
292             if ( !exists $help_for_sub{$subname} ) {
293             $c->register( $subname, $subs{$subname} );
294             }
295             }
296             }
297             }
298             }
299              
300             sub register_command { return register(@_) }
301              
302             sub register {
303             my ( $c, $command_name, $coderef, $extra ) = @_;
304              
305             # short circuit
306             return unless ref $coderef eq 'CODE';
307              
308             my %command_options = (
309             name => $command_name,
310             code => $coderef,
311             );
312              
313             # the extra parameter may be a help string
314             # or an argument hashref
315             if ($extra) {
316             if ( ref $extra ) {
317             $command_options{opts} = $extra;
318             }
319             else {
320             $command_options{help} = $extra;
321             }
322             }
323              
324             my $cmd_obj = App::Rad::Command->new( \%command_options );
325             return unless $cmd_obj;
326              
327             #TODO: I don't think this message is ever being printed (wtf?)
328             $c->debug("registering $command_name as a command.");
329              
330             $c->{'_commands'}->{$command_name} = $cmd_obj;
331             return $command_name;
332             }
333              
334             sub unregister_command { return unregister(@_) }
335              
336             sub unregister {
337             my ( $c, $command_name ) = @_;
338              
339             if ( $c->{'_commands'}->{$command_name} ) {
340             delete $c->{'_commands'}->{$command_name};
341             }
342             else {
343             return undef;
344             }
345             }
346              
347             sub commands {
348             return ( grep { $_ ne '' } keys %{ $_[0]->{'_commands'} } );
349             }
350              
351             sub is_command {
352             my ( $c, $cmd ) = @_;
353             return 0 unless defined $cmd and $cmd ne '';
354             return (
355             defined $c->{'_commands'}->{$cmd}
356             ? 1
357             : 0
358             );
359             }
360              
361             # TODO: turn 'command' into an alias for ->cmd
362             sub command : lvalue { $_[0]->{'cmd'} }
363             sub cmd : lvalue { $_[0]->{'cmd'} }
364              
365             # - "I'm loving having something else write up the 80% drudge
366             # code for the small things." (benh on Rad)
367             sub run {
368             my $class = shift;
369             my $c = {};
370             bless $c, $class;
371              
372             $c->_init();
373              
374             # first we update the control functions
375             # with any overriden value
376             $c->_register_functions();
377              
378             # then we run the setup to register
379             # some commands
380             $c->{'_functions'}->{'setup'}->($c);
381              
382             # now we get the actual input from
383             # the command line (someone using the app!)
384             my $arg = App::Rad::Parser::parse_input($c);
385             my $cmd_obj = $c->{'_commands'}->{$c->cmd};
386              
387             # handle special cases (default and invalid)
388             if ( defined $arg ) {
389             $c->debug( "'$arg' is not a valid command. Falling to invalid." );
390             $cmd_obj->{code} = $c->{'_functions'}->{'invalid'};
391             }
392             elsif ( $c->cmd eq '' ) {
393             $c->debug('no command detected. Falling to default');
394             $cmd_obj->{code} = $c->{'_functions'}->{'default'};
395             }
396              
397             # run the specified command
398             $c->_run_full_round($cmd_obj, $arg);
399              
400             # that's it. Tear down everything and go home :)
401             $c->{'_functions'}->{'teardown'}->($c);
402              
403             return 0;
404             }
405              
406             # run operations
407             # in a shell-like environment
408             sub shell {
409             my $class = shift;
410             require App::Rad::Shell;
411             App::Rad::Shell::shell($class, @_);
412             }
413              
414             sub execute {
415             my ( $c, $cmd ) = @_;
416              
417             # given command has precedence
418             if ($cmd) {
419             $c->{'cmd'} = $cmd;
420             }
421             else {
422             $cmd = $c->{'cmd'}; # now $cmd always has the called cmd
423             }
424              
425             # valid command, run it and return the command name
426             if ( $c->is_command($cmd) ) {
427             my $cmd_obj = $c->{'_commands'}->{$cmd};
428              
429             # set default values for command (if available)
430             App::Rad::Parser::set_defaults($cmd_obj);
431              
432             $c->_run_full_round( $cmd_obj, @_ );
433             return $cmd;
434             }
435             else {
436             # if not a command, return undef
437             return;
438             }
439             }
440              
441             sub argv { return $_[0]->{'_ARGV'} }
442             sub options { return $_[0]->{'_commands'}->{ $_[0]->{'cmd'} }->options }
443             sub stash { return $_[0]->{'_stash'} }
444             sub config { return $_[0]->{'_config'} }
445            
446             # get user information via prompting - Al Newkirk (awnstudio)
447             sub prompt { return App::Rad::Shell::prompt(@_); }
448              
449             # $c->plugins is sort of "read-only" externally
450             sub plugins {
451             my @plugins = @{ $_[0]->{'_plugins'} };
452             return @plugins;
453             }
454              
455             sub getopt {
456             require Getopt::Long;
457             Carp::croak "Getopt::Long needs to be version 2.36 or above"
458             unless $Getopt::Long::VERSION >= 2.36;
459              
460             my ( $c, @options ) = @_;
461              
462             # reset values from tinygetopt
463             #TODO: how the new parser copes with this?
464             %{ $c->options } = ();
465              
466             my $parser = new Getopt::Long::Parser;
467             $parser->configure(qw(bundling));
468              
469             my @tARGV = @ARGV; # we gotta stick to our API
470             #FIXME: line below doesn't work with new internal structure
471             my $ret = $parser->getoptions( $c->{'_options'}, @options );
472             @{ $c->argv } = @ARGV;
473             @ARGV = @tARGV;
474              
475             return $ret;
476             }
477              
478             sub debug {
479             if ( shift->{'debug'} ) {
480             print "[debug] @_\n";
481             }
482             }
483              
484             # gets/sets the output (returned value)
485             # of a command, to be post processed
486             sub output {
487             my ( $c, @msg ) = @_;
488             if (@msg) {
489             $c->{'output'} = join( ' ', @msg );
490             }
491             else {
492             return $c->{'output'};
493             }
494             }
495              
496             #=========================#
497             # CONTROL FUNCTIONS #
498             #=========================#
499              
500             sub setup { $_[0]->register_commands( { -ignore_prefix => '_' } ) }
501              
502             sub teardown {}
503              
504             sub pre_process {}
505              
506             sub post_process {
507             my $c = shift;
508              
509             if ( $c->output() ) {
510             print $c->output() . $/;
511             }
512             }
513              
514             sub default {
515             my $c = shift;
516             return $c->{'_commands'}->{'help'}->run($c);
517             }
518              
519             sub invalid {
520             my $c = shift;
521             return $c->{'_functions'}->{'default'}->($c);
522             }
523              
524             }
525             42; # ...and thus ends thy module ;)
526             __END__