File Coverage

blib/lib/App/Basis.pm
Criterion Covered Total %
statement 137 174 78.7
branch 53 96 55.2
condition 11 17 64.7
subroutine 22 29 75.8
pod 12 13 92.3
total 235 329 71.4


line stmt bran cond sub pod time code
1             # ABSTRACT: Simple way to create applications
2              
3              
4             package App::Basis;
5             $App::Basis::VERSION = '1.0';
6 2     2   67611 use 5.014;
  2         8  
  2         69  
7 2     2   19 use warnings;
  2         3  
  2         60  
8 2     2   10 use strict;
  2         8  
  2         56  
9 2     2   2383 use Getopt::Long;
  2         25756  
  2         13  
10 2     2   631 use Exporter;
  2         6  
  2         75  
11 2     2   1093 use File::HomeDir;
  2         6649  
  2         113  
12 2     2   13 use Path::Tiny;
  2         3  
  2         385  
13 2     2   2544 use IPC::Cmd qw(run run_forked);
  2         170942  
  2         168  
14 2     2   25 use List::Util qw(max);
  2         4  
  2         197  
15 2     2   11 use POSIX qw(strftime);
  2         2  
  2         19  
16              
17 2     2   1096 use vars qw( @EXPORT @ISA);
  2         4  
  2         4883  
18              
19             @ISA = qw(Exporter);
20              
21             # this is the list of things that will get imported into the loading packages
22             # namespace
23             @EXPORT = qw(
24             init_app
25             show_usage
26             msg_exit
27             get_program
28             debug set_debug
29             daemonise
30             execute_cmd run_cmd
31             set_log_file
32             fix_filename
33             set_test_mode
34             );
35              
36             # ----------------------------------------------------------------------------
37              
38             my $PROGRAM = path($0)->basename;
39             my $LOG_FILE = fix_filename("~/$PROGRAM.log");
40              
41             # these variables are held available throughout the life of the app
42             my $_app_simple_ctrlc_count = 0;
43             my $_app_simple_ctrlc_handler;
44             my $_app_simple_help_text = 'Application has not defined help_text yet.';
45             my $_app_simple_help_options = '';
46             my $_app_simple_cleanup_func;
47             my $_app_simple_help_cmdline = '';
48              
49             my %_app_simple_objects = ();
50             my %_cmd_line_options = ();
51              
52             # we may want to die rather than exiting, helps with testing!
53             my $_test_mode = 0;
54              
55              
56             # ----------------------------------------------------------------------------
57             # control how we output things to help with testing
58             sub _output {
59 3     3   8 my ( $where, $msg ) = @_;
60              
61 3 50       10 if ( !$_test_mode ) {
62 0 0       0 if ( $where =~ /stderr/i ) {
63 0         0 say STDERR $msg;
64             }
65             else {
66 0         0 say $msg ;
67             }
68             }
69             }
70              
71             # ----------------------------------------------------------------------------
72              
73              
74             sub set_log_file {
75 1     1 1 6 my ($file) = @_;
76 1         2 $LOG_FILE = $file;
77             }
78              
79             # ----------------------------------------------------------------------------
80              
81              
82             sub debug {
83 0     0 1 0 my ( $level, @debug ) = @_;
84              
85             # we may want to undef the debug object, so no debug comes out
86              
87 0 0       0 if ( exists $_app_simple_objects{logger} ) {
88              
89             # run the coderef for the logger
90 0 0       0 $_app_simple_objects{logger}->( $level, @debug ) if ( defined $_app_simple_objects{logger} );
91             }
92             else {
93 0         0 path($LOG_FILE)->append_utf8( strftime( '%Y-%m-%d %H:%M:%S', gmtime( time() ) ) . " [$level] " . join( ' ', @debug ) . "\n");
94             }
95             }
96              
97             # ----------------------------------------------------------------------------
98              
99              
100             sub set_debug {
101 0     0 1 0 my $func = shift;
102 0 0 0     0 if ( !$func || ref($func) ne "CODE" ) {
103 0         0 warn "set_debug function expects a CODE, got a " . ref($func);
104             }
105             else {
106 0         0 $_app_simple_objects{logger} = $func;
107             }
108             }
109              
110             # ----------------------------------------------------------------------------
111              
112              
113             sub init_app {
114 11 50   11 1 6575 my %args = @_ % 2 ? die("Odd number of values passed where even is expected.") : @_;
115 11         16 my @options;
116 11         16 my $has_required = 0;
117 11         16 my %full_options;
118              
119 11 50       32 if ( $args{log_file} ) {
120 0         0 $LOG_FILE = fix_filename( $args{log_file} );
121             }
122              
123 11 50       26 if ( $args{debug} ) {
124 0         0 set_debug( $args{debug} );
125             }
126              
127             # get program description
128 11 50       32 $_app_simple_help_text = $args{help_text} if ( $args{help_text} );
129 11 50       27 $_app_simple_help_cmdline = $args{help_cmdline} if ( $args{help_cmdline} );
130              
131 11 50       33 die "options must be a hashref" if ( ref( $args{options} ) ne 'HASH' );
132              
133 11         29 $args{options}->{'help|h|?'} = 'Show help';
134              
135 11         12 my @keys = sort keys %{ $args{options} };
  11         61  
136 11         50 my %dnames = _desc_names(@keys);
137 11         62 my $max_desc_len = max( map length, values %dnames ) + 1;
138 11         26 my $help_fmt = " %-${max_desc_len}s %s\n";
139              
140             # add help text for 'help' first.
141 11         54 $_app_simple_help_options .= sprintf $help_fmt, $dnames{'help|h|?'}, 'Show help';
142              
143             # get options and their descriptions
144 11         19 foreach my $o (@keys) {
145              
146             # save the option
147 25         40 push @options, $o;
148              
149 25         32 my $name = $o;
150              
151             # we want the long version of the name if its provided
152 25         114 $name =~ s/.*?(\w+).*/$1/;
153              
154             # remove any type data
155 25         38 $name =~ s/=(.*)//;
156              
157 25 100       73 if ( ref( $args{options}->{$o} ) eq 'HASH' ) {
158 15 100       56 die "parameterised option '$name' require a desc option"
159             if ( !$args{options}->{$o}->{desc} );
160 14         26 $full_options{$name} = $args{options}->{$o};
161 14 100       41 $has_required++ if ( $full_options{$name}->{required} );
162             }
163             else {
164 10         38 $full_options{$name} = {
165             desc => $args{options}->{$o},
166              
167             # possible options that can be passed
168             # depends => '',
169             # default => '',
170             # required => 0,
171             # validate => sub {}
172             };
173             }
174              
175             # save the option string too
176 24         46 $full_options{$name}->{options} = $o;
177              
178             # build the entry for the help text
179 24         38 my $desc = $full_options{$name}->{desc};
180 24 100       66 if ( $name ne 'help' ) {
181 14         21 my $desc = $full_options{$name}->{desc};
182              
183             # show the right way to use the options
184 14         23 my $dname = $dnames{$o};
185 14 100       34 $dname .= '*' if ( $full_options{$name}->{required} );
186              
187 14 100       37 $desc .= " [DEFAULT: $full_options{$name}->{default}]" if ( $full_options{$name}->{default} );
188 14         57 $_app_simple_help_options .= sprintf $help_fmt, $dname, $desc;
189             }
190              
191             }
192              
193             # show required options
194 10 100       26 if ($has_required) {
195 8 50       24 $_app_simple_help_options .= "* required option" . ( $has_required > 1 ? 's' : '' ) . "\n";
196             }
197              
198             # catch control-c, user provided or our default
199 10 50       28 $_app_simple_ctrlc_handler = $args{ctrl_c} ? $args{ctrl_c} : \&_app_simple_ctrlc_func;
200 10         116 $SIG{'INT'} = $_app_simple_ctrlc_handler;
201              
202             # get an cleanup function handler
203 10 50       23 $_app_simple_cleanup_func = $args{cleanup} if ( $args{cleanup} );
204              
205             # check command line args
206 10         40 GetOptions( \%_cmd_line_options, @options );
207              
208             # help is a built in
209 10 50       2724 show_usage() if ( $_cmd_line_options{help} );
210              
211             # now if we have the extended version we can do some checking
212 10         36 foreach my $name ( sort keys %full_options ) {
213 20 50       50 warn "Missing desc field for $name" if ( !$full_options{$name}->{desc} );
214 20 100       46 if ( $full_options{$name}->{required} ) {
215 8 100 66     37 show_usage( "Required option '$name' is missing", 1 ) if ( !( $_cmd_line_options{$name} || $full_options{$name}->{default} ) );
216             }
217 19 100       45 if ( $full_options{$name}->{depends} ) {
218 6 100       18 if ( !$_cmd_line_options{ $full_options{$name}->{depends} } ) {
219 1         7 show_usage( "Option '$name' depends on option '$full_options{$name}->{depends}' but it is missing", 1 );
220             }
221             }
222              
223             # set a default if there is no value
224 18 100       43 if ( $full_options{$name}->{default} ) {
225 4 50       11 $_cmd_line_options{$name} = $full_options{$name}->{default} if ( !$_cmd_line_options{$name} );
226             }
227              
228             # call the validation routine if we have one
229 18 100 100     83 if ( $_cmd_line_options{$name} && $full_options{$name}->{validate} ) {
230 3 50       13 die "need to pass a coderef to validate for option '$name'" if ( !ref( $full_options{$name}->{validate} ) eq 'CODE' );
231 3 50 33     11 die "Option '$name' has validate and should either also have a default or be required"
232             if ( !( $full_options{$name}->{required} || $full_options{$name}->{default} ) );
233 3         8 my $coderef = $full_options{$name}->{validate};
234 3         26 my $result = $coderef->( $_cmd_line_options{$name} );
235 2 100       19 show_usage("Option '$name' does not pass validation") if ( !$result );
236             }
237             }
238              
239 6         45 return %_cmd_line_options;
240             }
241              
242             # ----------------------------------------------------------------------------
243              
244              
245             sub get_program {
246 1     1 1 792 return $PROGRAM;
247             }
248              
249             # ----------------------------------------------------------------------------
250              
251              
252             sub get_options {
253 0     0 1 0 return %_cmd_line_options;
254             }
255              
256             # ----------------------------------------------------------------------------
257             # handle the ctrl-c presses
258              
259             sub _app_simple_ctrlc_func {
260              
261             # exit if we are already in ctrlC
262 0 0   0   0 exit(2) if ( $_app_simple_ctrlc_count++ );
263 0         0 _output( 'STDERR', "\nCaught Ctrl-C. press again to exit immediately" );
264              
265             # re-init the handler
266 0         0 $SIG{'INT'} = $_app_simple_ctrlc_handler;
267             }
268              
269             # ----------------------------------------------------------------------------
270              
271             # to help with testing we may want to die, which can be caught rather than
272             # exiting, so lets find out
273              
274             sub _exit_or_die {
275 3   100 3   12 my $state = shift || 1;
276              
277 3 50       8 if ($_test_mode) {
278 3         79 STDERR->flush();
279 3         15 STDOUT->flush();
280 3         42 die "exit state $state";
281             }
282 0         0 exit($state);
283             }
284              
285             # ----------------------------------------------------------------------------
286              
287              
288             sub show_usage {
289 3     3 1 7 my ( $msg, $state ) = @_;
290              
291 3         17 my $help = qq{
292             Syntax: $PROGRAM [options] $_app_simple_help_cmdline
293              
294             About: $_app_simple_help_text
295              
296             [options]
297             $_app_simple_help_options};
298 3 50       9 if ($msg) {
299              
300             # if we have an error message it MUST go to STDERR
301             # to make sure that any program the output is piped to
302             # does not get the message to process
303 3         16 _output( 'STDERR', "$help\nError: $msg\n" );
304             }
305             else {
306 0         0 _output( 'STDOUT', $help );
307             }
308              
309 3         7 _exit_or_die($state);
310             }
311              
312             # ----------------------------------------------------------------------------
313              
314              
315             sub msg_exit {
316 0     0 1 0 my ( $msg, $state ) = @_;
317              
318 0 0       0 _output( 'STDERR', $msg ) if ($msg);
319 0         0 _exit_or_die($state);
320             }
321              
322             # -----------------------------------------------------------------------------
323              
324              
325             sub daemonise {
326 0     0 1 0 my $rootdir = shift;
327              
328 0 0       0 if ($rootdir) {
329 0 0       0 chroot($rootdir)
330             or die "Could not chroot to $rootdir, only the root user can do this.";
331             }
332              
333             # fork once and let the parent exit
334 0         0 my $pid = fork();
335              
336             # exit if $pid ;
337             # parent to return 0, as it is logical
338 0 0       0 if ($pid) {
339 0         0 return 0;
340             }
341 0 0       0 die "Couldn't fork: $!" unless defined $pid;
342              
343             # disassociate from controlling terminal, leave the
344             # process group behind
345              
346 0 0       0 POSIX::setsid() or die "Can't start a new session";
347              
348             # show that we have started a daemon process
349 0         0 return 1;
350             }
351              
352             # ----------------------------------------------------------------------------
353              
354              
355             sub execute_cmd {
356 0     0 1 0 my %args = @_;
357 0 0       0 my $command = $args{command} or die "command required";
358              
359 0         0 my $r = IPC::Cmd::run_forked( $command, \%args );
360              
361 0         0 return $r;
362             }
363              
364             # ----------------------------------------------------------------------------
365              
366              
367             sub run_cmd {
368 2     2 1 893 my $cmd = shift;
369              
370             # use our local version of path so that it can pass taint checks
371 2         18 local $ENV{PATH} = $ENV{PATH};
372              
373 2         15 my ( $ret, $err, $full_buff, $stdout_buff, $stderr_buff ) = run( command => $cmd );
374              
375 2         34534 my $stdout = join( "\n", @{$stdout_buff} );
  2         14  
376 2         9 my $stderr = join( "\n", @{$stderr_buff} );
  2         11  
377              
378 2         90 return ( !$ret, $stdout, $stderr );
379             }
380              
381             # -----------------------------------------------------------------------------
382              
383              
384             sub fix_filename {
385 9     9 1 6642 my $file = shift;
386 9 50       44 return if ( !$file );
387              
388 9         78 my $home = File::HomeDir->my_home;
389 9         572 $file =~ s/^~/$home/;
390 9 100       33 if ( $file =~ m|^\.\./| ) {
391 1         11 my $parent = path( Path::Tiny->cwd )->dirname;
392 1         130 $file =~ s|^(\.{2})/|$parent/|;
393             }
394 9 100 100     75 if ( $file =~ m|^\./| || $file eq '.' ) {
395 2         19 my $cwd = Path::Tiny->cwd;
396 2         104 $file =~ s|^(\.)/?|$cwd|;
397             }
398              
399             # replace multiple separators
400 9         36 $file =~ s|//|/|g;
401 9         31 return $file;
402             }
403              
404             # ----------------------------------------------------------------------------
405             # Returns a hash containing a formatted name for each option. For example:
406             # ( 'help|h|?' ) -> { 'help|h|?' => '-h, -?, --help' }
407             sub _desc_names {
408 11     11   14 my %descs;
409 11         24 foreach my $o (@_) {
410 26         35 $_ = $o; # Keep a copy of key in $o.
411 26         71 s/=.*$//;
412              
413             # Sort by length so single letter options are shown first.
414 26         90 my @parts = sort { length $a <=> length $b } split /\|/;
  33         67  
415              
416             # Single chars get - prefix, names get -- prefix.
417 26 100       54 my $s = join ", ", map { ( length > 1 ? '--' : '-' ) . $_ } @parts;
  48         150  
418              
419 26         87 $descs{$o} = $s;
420             }
421 11         49 return %descs;
422             }
423              
424             # ----------------------------------------------------------------------------
425             # special function to help us test this module, as it flags that we can die
426             # rather than exiting when doing some operations
427             # also test mode will not output to STDERR/STDOUT
428              
429             sub set_test_mode {
430 1     1 0 12 $_test_mode = shift;
431             }
432              
433             # ----------------------------------------------------------------------------
434             # make sure we do any cleanup required
435              
436             END {
437              
438             # call any user supplied cleanup
439 2 50   2   635 if ($_app_simple_cleanup_func) {
440 0         0 $_app_simple_cleanup_func->();
441 0         0 $_app_simple_cleanup_func = undef;
442             }
443             }
444              
445              
446             # ----------------------------------------------------------------------------
447              
448             1;
449              
450             __END__