File Coverage

blib/lib/CLI/Helpers.pm
Criterion Covered Total %
statement 166 364 45.6
branch 87 214 40.6
condition 36 110 32.7
subroutine 31 43 72.0
pod 15 15 100.0
total 335 746 44.9


line stmt bran cond sub pod time code
1             package CLI::Helpers;
2             # ABSTRACT: Subroutines for making simple command line scripts
3             # RECOMMEND PREREQ: App::Nopaste
4             # RECOMMEND PREREQ: Term::ReadLine::Gnu
5              
6 2     2   174347 use strict;
  2         3  
  2         69  
7 2     2   8 use feature qw(state);
  2         2  
  2         194  
8 2     2   10 use warnings;
  2         3  
  2         105  
9              
10 2     2   578 use Capture::Tiny qw(capture);
  2         19820  
  2         105  
11 2     2   10 use File::Basename;
  2         2  
  2         122  
12 2     2   1179 use Getopt::Long qw(GetOptionsFromArray);
  2         18669  
  2         7  
13 2     2   1060 use IO::Interactive qw( is_interactive );
  2         1528  
  2         11  
14 2     2   830 use JSON::MaybeXS;
  2         15123  
  2         120  
15 2     2   838 use Module::Load qw(load);
  2         1954  
  2         11  
16 2     2   904 use Ref::Util qw(is_ref is_arrayref is_hashref);
  2         3763  
  2         132  
17 2     2   1183 use Sys::Syslog qw(:standard);
  2         36258  
  2         259  
18 2     2   1417 use Term::ANSIColor 2.01 qw(color colored colorstrip);
  2         13927  
  2         1355  
19 2     2   908 use Term::ReadKey;
  2         3479  
  2         134  
20 2     2   928 use Term::ReadLine;
  2         4832  
  2         64  
21 2     2   1009 use YAML::XS ();
  2         6601  
  2         139  
22              
23             our $VERSION = '2.2'; # VERSION
24              
25             # Capture ARGV at Load
26             my @ORIG_ARGS;
27             BEGIN {
28 2     2   763 @ORIG_ARGS = @ARGV;
29             }
30              
31              
32             require Exporter;
33             our @ISA = qw(Exporter);
34              
35             my @export_output = qw(output verbose debug debug_var);
36             my @export_input = qw(prompt menu text_input confirm pwprompt);
37             my @export_always = qw(cli_helpers_initialize options_description);
38              
39             our @EXPORT_OK = ( @export_output, @export_input, @export_always );
40             our %EXPORT_TAGS = (
41             all => [@export_always, @export_input, @export_output],
42             input => [@export_always, @export_input],
43             output => [@export_always, @export_output],
44             );
45              
46             my $ARGV_AT_INIT = 0;
47             my $COPY_ARGV = 0;
48             our $_init_complete = 0;
49              
50             sub import {
51 1     1   10 my (@args) = @_;
52              
53 1         1 my @import = ();
54             # We need to process the config options
55 1         1 my $explicit_argv = 0;
56 1         2 foreach my $arg ( @args ) {
57 3 100       8 if( $arg eq 'delay_argv' ) {
    50          
    50          
58 1         1 $ARGV_AT_INIT = 0;
59 1         2 $explicit_argv++;
60             }
61             elsif( $arg eq 'preprocess_argv' ) {
62 0         0 $ARGV_AT_INIT = 1;
63 0         0 $explicit_argv++;
64             }
65             elsif( $arg eq 'copy_argv' ) {
66 0         0 $COPY_ARGV = 1;
67             }
68             # Not a config option, pass through
69             else {
70 2         3 push @import, $arg;
71             }
72             }
73 1 50       2 if ( $explicit_argv != 1 ) {
74 0         0 my ($package) = caller;
75 0         0 $ARGV_AT_INIT = $package eq 'main';
76             }
77              
78 1         172 CLI::Helpers->export_to_level( 1, @import );
79             }
80              
81             {
82             ## no critic (ProhibitNoWarnings)
83 2     2   15 no warnings;
  2         3  
  2         12557  
84             INIT {
85 1 50   1   257034 return if $_init_complete++;
86 1 50       16 cli_helpers_initialize() if $ARGV_AT_INIT;
87             }
88             ## use critic
89             }
90              
91              
92             my %OPTIONS = (
93             color => {
94             description => "Enable colorized output",
95             reversible => 1,
96             },
97             'data-file' => {
98             description => "Filename for output tagged as data",
99             format => "s",
100             },
101             debug => {
102             description => "Show developer output",
103             },
104             'debug-class' => {
105             description => "Show developer output for a specific class",
106             format => "s",
107             },
108             nopaste => {
109             description => "Paste output to configured paste service",
110             },
111             'nopaste-public' => {
112             description => "Must be set to use public paste services",
113             },
114             'nopaste-service' => {
115             description => "Comma-separated App::Nopaste services",
116             format => "s",
117             },
118             quiet => {
119             description => "Suppress output to STDERR and STDOUT",
120             },
121             syslog => {
122             description => "Generate messages to syslog as well",
123             },
124             'syslog-debug' => {
125             description => "Enable debug messages to syslog if in use",
126             },
127             'syslog-facility' => {
128             description => "Syslog facility, defaults to 'local0'",
129             format => "s",
130             },
131             'syslog-tag' => {
132             description => "Syslog program name, defaults to script name",
133             format => "s",
134             },
135             tags => {
136             description => "A comma separated list of tags to display",
137             format => "s",
138             },
139             verbose => {
140             aliases => [qw(v)],
141             description => "Incremental, increase verbosity output",
142             incremental => 1,
143             },
144             );
145              
146             sub _parse_options {
147 13     13   21 my ($opt_ref) = @_;
148 13         14 my @opt_spec;
149              
150 13         118 foreach my $opt (sort keys %OPTIONS) {
151 182         217 my $def = $OPTIONS{$opt};
152 182 100       312 my $spec = join("|", $opt, $def->{aliases} ? @{ $def->{aliases} } : ());
  13         34  
153 182 100       374 if ( $def->{format} ) {
    100          
    100          
154 78         102 $spec .= "=$def->{format}";
155             } elsif ( $def->{incremental} ) {
156 13         19 $spec .= "+";
157             } elsif ( $def->{reversible} ) {
158 13         19 $spec .= "!";
159             }
160 182         312 push @opt_spec, $spec;
161             }
162              
163 13         33 my $argv;
164             my %opt;
165 13 100 66     54 if( defined $opt_ref && is_arrayref($opt_ref) ) {
166             # If passed an argv array, use that
167 12 50       21 $argv = $COPY_ARGV ? [ @{ $opt_ref } ] : $opt_ref;
  0         0  
168             }
169             else {
170 1 50       8 $argv = $COPY_ARGV ? [ @ARGV ] : \@ARGV;
171             }
172             # Set pass_through and save previous settings
173 13         46 my $prev = Getopt::Long::Configure('pass_through');
174             eval {
175 13         44 GetOptionsFromArray($argv, \%opt, @opt_spec );
176 13 50       386 } or do {
177 0         0 my $err = $@;
178 0         0 warn "CLI::Helpers::_parse_options failed: $err";
179             };
180             # Restore previous settings
181 13         12792 Getopt::Long::Configure($prev);
182 13         300 return \%opt;
183             }
184              
185             my $DATA_HANDLE = undef;
186             sub _open_data_file {
187 0     0   0 my $data_file = shift;
188             eval {
189 0 0       0 open($DATA_HANDLE, '>', $data_file) or die "data file unwritable: $!";
190 0         0 1;
191 0 0       0 } or do {
192 0         0 my $error = $@;
193 0         0 output({color=>'red',stderr=>1}, "Attempted to write to $data_file failed: $!");
194             };
195             }
196              
197              
198             # Set defaults
199             my %DEF = ();
200             my $TERM = undef;
201             my @STICKY = ();
202             my @NOPASTE = ();
203             my %TAGS = ();
204              
205              
206             sub cli_helpers_initialize {
207 14     14 1 10350 my ($argv) = @_;
208              
209 14 100       47 state $defaults = _parse_options() unless $argv;
210 14 100       42 my $opts = $argv ? _parse_options($argv) : $defaults;
211 14 50       42 _open_data_file($opts->{'data-file'}) if $opts->{'data-file'};
212              
213             # Initialize Global Definitions
214             %DEF = (
215             DEBUG => $opts->{debug} || 0,
216             DEBUG_CLASS => $opts->{'debug-class'} || 'main',
217             ENCODE => lc($opts->{encode} || 'json'),
218             VERBOSE => $opts->{verbose} || 0,
219             KV_FORMAT => ': ',
220             QUIET => $opts->{quiet} || 0,
221             SYSLOG => $opts->{syslog} || 0,
222             SYSLOG_TAG => exists $opts->{'syslog-tag'} && length $opts->{'syslog-tag'} ? $opts->{'syslog-tag'} : basename($0),
223             SYSLOG_FACILITY => exists $opts->{'syslog-facility'} && length $opts->{'syslog-facility'} ? $opts->{'syslog-facility'} : 'local0',
224             SYSLOG_DEBUG => $opts->{'syslog-debug'} || 0,
225 0         0 TAGS => $opts->{tags} ? { map { $_ => 1 } split /,/, $opts->{tags} } : undef,
226             NOPASTE => $opts->{nopaste} || 0,
227             NOPASTE_SERVICE => $opts->{'nopaste-service'},
228 14 50 100     943 NOPASTE_PUBLIC => $opts->{'nopaste-public'},
    50 50        
    50 50        
      100        
      50        
      50        
      33        
      33        
      50        
      50        
229             );
230 14   33     67 $DEF{COLOR} = $opts->{color} // git_color_check();
231 14 50 33     279 $DEF{ENCODE} = 'json' unless $DEF{ENCODE} eq 'json' or $DEF{ENCODE} eq 'yaml';
232              
233 14         34 debug("DEFINITIONS");
234 14         60 debug_var(\%DEF);
235              
236             # Setup the Syslog Subsystem
237 14 50       31 if( $DEF{SYSLOG} ) {
238             eval {
239 0         0 openlog($DEF{SYSLOG_TAG}, 'ndelay,pid', $DEF{SYSLOG_FACILITY});
240 0         0 1;
241 0 0       0 } or do {
242 0         0 my $error = $@;
243 0         0 $DEF{SYSLOG}=0;
244 0         0 output({stderr=>1,color=>'red'}, "CLI::Helpers could not open syslog: $error");
245             };
246             }
247              
248             # Optionally Attempt Loading App::NoPaste
249 14 50       27 if( $DEF{NOPASTE} ) {
250             eval {
251 0         0 load 'App::Nopaste';
252 0         0 1;
253 0 0       0 } or do {
254 0         0 $DEF{NOPASTE} = 0;
255 0         0 output({stderr=>1,color=>'red',sticky=>1},
256             'App::Nopaste is not installed, please cpanm App::Nopaste for --nopaste support',
257             );
258             };
259             }
260              
261 14         37 return 1;
262             }
263              
264              
265             # Allow some messages to be fired at the end the of program
266             END {
267             # Show discovered tags
268 2 50   2   2820 if( keys %TAGS ) {
269             output({color=>'cyan',stderr=>1},
270             sprintf "# Tags discovered: %s",
271 0         0 join(', ', map { "$_=$TAGS{$_}" } sort keys %TAGS)
  0         0  
272             );
273             }
274             # Show Sticky Output
275 2 50       9 if(@STICKY) {
276 0         0 foreach my $args (@STICKY) {
277 0         0 output(@{ $args });
  0         0  
278             }
279             }
280             # Do the Nopaste
281 2 50       7 if( @NOPASTE ) {
282 0         0 my $command_string = join(" ", $0, @ORIG_ARGS);
283 0         0 unshift @NOPASTE, "\$ $command_string";
284             # Figure out what services to use
285             my $services = $DEF{NOPASTE_SERVICE} ? [ split /,/, $DEF{NOPASTE_SERVICE} ]
286 0 0       0 : $ENV{NOPASTE_SERVICES} ? [ split /,/, $ENV{NOPASTE_SERVICES} ]
    0          
287             : undef;
288             my %paste = (
289             text => join("\n", @NOPASTE),
290             summary => $command_string,
291             desc => $command_string,
292             # Default to a Private Paste
293 0 0       0 private => $DEF{NOPASTE_PUBLIC} ? 0 : 1,
294             );
295 0         0 debug_var(\%paste);
296 0 0       0 if( $services ) {
297 0         0 output({color=>'cyan',stderr=>1}, "# NoPaste: "
298             . App::Nopaste->nopaste(%paste, services => $services)
299             );
300             }
301             else {
302 0         0 output({color=>'red',stderr=>1,clear=>1},
303             "!! In order to use --nopaste, you need to your environment variable",
304             "!! NOPASTE_SERVICES or pass --nopaste-service, e.g.:",
305             "!! export NOPASTE_SERVICES=Shadowcat,PastebinCom");
306             }
307             }
308 2 50       25 closelog() if $DEF{SYSLOG};
309             }
310              
311              
312             sub options_description {
313 0     0 1 0 my @description = (
314             [],
315             ["CLI::Helpers Options"],
316             );
317              
318 0         0 my(@opt,@desc);
319 0         0 my $opt_width = 0;
320 0         0 foreach my $opt ( sort keys %OPTIONS ) {
321 0         0 my $def = $OPTIONS{$opt};
322 0         0 my $desc = sprintf "--%s", $opt;
323 0 0       0 if ( $def->{aliases} ) {
324 0         0 $desc .= ' (';
325 0         0 foreach my $alias ( @{ $def->{aliases} } ) {
  0         0  
326 0 0       0 $desc .= sprintf "or %s%s", length($alias) > 1 ? '--' : '-', $alias;
327             }
328 0         0 $desc .= ')';
329             }
330 0         0 push @opt, $desc;
331 0 0       0 $opt_width = length($desc) if length($desc) > $opt_width;
332 0         0 push @desc, $def->{description};
333 0 0       0 if ( $def->{reversible} ) {
334 0         0 push @opt, sprintf "--no%s", $opt;
335 0         0 push @desc, $def->{description} =~ s/Enable/Disable/r;
336             }
337             }
338              
339 0   0     0 while( @opt && @desc ) {
340 0         0 push @description, [sprintf "%-${opt_width}s %s", shift(@opt), shift(@desc) ];
341             }
342              
343 0         0 return @description;
344             }
345              
346              
347             sub colorize {
348 24     24 1 42 my ($color,$string) = @_;
349              
350 24 0 33     45 if( defined $color && $DEF{COLOR} ) {
351 0         0 $string=colored([ $color ], $string);
352             }
353 24         54 return $string;
354             }
355              
356              
357             sub git_color_check {
358 14 50   14 1 50 return unless is_interactive();
359              
360 0         0 my @cmd = qw(git config --global --get color.ui);
361             my($stdout,$stderr,$rc) = capture {
362 0     0   0 system @cmd;
363 0         0 };
364 0 0       0 if( $rc != 0 ) {
365 0         0 debug("git_color_check error: $stderr");
366 0         0 return 0;
367             }
368 0         0 debug("git_color_check out: $stdout");
369 0 0 0     0 if( $stdout =~ /auto/ || $stdout =~ /true/ ) {
370 0         0 return 1;
371             }
372              
373 0         0 return 0;
374             }
375              
376              
377 2 50   2 1 21 sub def { return exists $DEF{$_[0]} ? $DEF{$_[0]} : undef }
378              
379              
380             my %_valid_opts = map { $_ => 1 } qw(_caller_package clear color data encode indent json kv level no_syslog stderr sticky syslog_level tag yaml);
381             sub _output_args {
382 23     23   47 my @args = @_;
383              
384 23 50       57 return unless @args;
385              
386 23 100       55 if ( @args == 1 ) {
387 9         47 return {}, $args[0];
388             }
389              
390 14 50       33 if ( is_hashref($args[0]) ) {
391 14         19 my $invalid = 0;
392 14         20 foreach my $k ( keys %{ $args[0] } ) {
  14         44  
393 24 100       57 next if exists $_valid_opts{$k};
394 1         1 $invalid = 1;
395 1         2 last;
396             }
397 14 100       32 return {}, @args if $invalid;
398             }
399              
400 13         55 return @args;
401             }
402              
403             sub output {
404 23     23 1 15361 my ($opts,@lines) = _output_args(@_);
405              
406 23         49 state $json = JSON->new->canonical->utf8->allow_blessed->convert_blessed;
407              
408             # Return unless we have something to work with;
409 23 50       64 return unless @lines;
410              
411             # Ensure we're all setup
412 23 50       43 cli_helpers_initialize() unless keys %DEF;
413              
414             # Input/output Arrays
415 23 50 33 8   138 my $encode = sub { $DEF{ENCODE} eq 'yaml' || $opts->{yaml} ? YAML::XS::Dump($_[0]) : $json->encode($_[0]) };
  8         78  
416 24 50       58 my @input = map { my $x=$_; chomp($x) if defined $x; $x; }
  24         57  
  24         104  
417 23 100 66     66 map { defined $_ && ref $_ ? $encode->($_) : $_ } @lines;
  24         112  
418 23         43 my @output = ();
419              
420             # Determine the color
421 23 50 33     63 my $color = exists $opts->{color} && defined $opts->{color} ? $opts->{color} : undef;
422              
423             # Determine indentation
424 23 50       53 my $indent = exists $opts->{indent} ? " "x(2*$opts->{indent}) : '';
425              
426             # If tagged, we only output if the tag is requested
427 23 0 33     53 if( $DEF{TAGS} && exists $opts->{tag} ) {
428             # Skip this altogether
429 0   0     0 $TAGS{$opts->{tag}} ||= 0;
430 0         0 $TAGS{$opts->{tag}}++;
431 0 0       0 return unless $DEF{TAGS}->{$opts->{tag}};
432             }
433              
434             # Determine if we're doing Key Value Pairs
435 23 50 66     72 my $DO_KV = (scalar(@input) % 2 == 0 ) && (exists $opts->{kv} && $opts->{kv} == 1) ? 1 : 0;
436              
437 23 50       41 if( $DO_KV ) {
438 0         0 while( @input ) {
439 0         0 my $k = shift @input;
440             # We only colorize the value
441 0         0 my $v = shift @input;
442 0 0 0     0 $v ||= $DEF{KV_FORMAT} eq ': ' ? '~' : '';
443 0         0 push @output, join($DEF{KV_FORMAT}, $k, colorize($color,$v));
444             }
445             }
446             else {
447 23         36 @output = map { colorize($color, $_) } @input;
  24         52  
448             }
449              
450             # Out to the console
451 23 50 33     63 if( !$DEF{QUIET} || $opts->{IMPORTANT} ) {
452 23 100       56 my $out_handle = $opts->{stderr} ? \*STDERR : \*STDOUT;
453             # Do clearing
454 23 100       119 print $out_handle "\n"x$opts->{clear} if exists $opts->{clear};
455             # Print output
456 23         1010 print $out_handle "${indent}$_\n" for @output;
457             }
458              
459             # Handle data, which is raw
460 23 50 33     1874 if(defined $DATA_HANDLE && $opts->{data}) {
    50 33        
461 0         0 print $DATA_HANDLE "$_\n" for @input;
462             }
463             elsif( $DEF{SYSLOG} && !$opts->{no_syslog}) {
464             my $level = exists $opts->{syslog_level} ? $opts->{syslog_level} :
465 0 0       0 exists $opts->{stderr} ? 'err' :
    0          
466             'notice';
467              
468             # Warning for syslogging data file
469             unshift @output, "CLI::Helpers logging a data section, use --data-file to suppress this in syslog."
470 0 0       0 if $opts->{data};
471              
472             # Now syslog the message
473 0         0 debug({no_syslog=>1,color=>'magenta'}, sprintf "[%s] Syslogging %d messages, with: %s", $level, scalar(@output), join(",", map { $_=>$opts->{$_} } keys %{ $opts }));
  0         0  
  0         0  
474 0         0 for( @output ) {
475             # One bad message means no more syslogging
476             eval {
477 0         0 syslog($level, colorstrip($_));
478 0         0 1;
479 0 0       0 } or do {
480 0         0 my $error = $@;
481 0         0 $DEF{SYSLOG} = 0;
482 0         0 output({stderr=>1,color=>'red',no_syslog=>1}, "syslog() failed: $error");
483             };
484             }
485             }
486              
487             # Sticky messages don't just go away
488 23 50       54 if(exists $opts->{sticky}) {
489 0         0 my %o = %{ $opts }; # Make a copy because we shifted this off @_
  0         0  
490             # So this doesn't happen in the END block again
491 0         0 delete $o{$_} for grep { exists $o{$_} } qw(sticky data);
  0         0  
492 0         0 $o{no_syslog} = 1;
493 0         0 push @STICKY, [ \%o, @input ];
494             }
495 23 50       233 if( $DEF{NOPASTE} ) {
496 0         0 push @NOPASTE, map { $indent . colorstrip($_) } @output;
  0         0  
497             }
498             }
499              
500              
501             sub verbose {
502 10 100   10 1 63 my $opts = is_hashref($_[0]) ? shift @_ : {};
503 10 100       33 $opts->{level} = 1 unless exists $opts->{level};
504 10 100       31 $opts->{syslog_level} = $opts->{level} > 1 ? 'debug' : 'info';
505 10         26 my @msgs=@_;
506              
507             # Ensure we're all configured
508 10 50       23 cli_helpers_initialize() unless keys %DEF;
509              
510 10 100       24 if( !$DEF{DEBUG} ) {
511 8 100       28 return unless $DEF{VERBOSE} >= $opts->{level};
512             }
513 5         13 output( $opts, @msgs );
514             }
515              
516              
517             sub debug {
518 34 100   34 1 99 my $opts = is_hashref($_[0]) ? shift @_ : {};
519 34         69 my @msgs=@_;
520              
521             # Ensure we're all configured
522 34 50       68 cli_helpers_initialize() unless keys %DEF;
523              
524             # Smarter handling of debug output
525 34 100       98 return unless $DEF{DEBUG};
526              
527             # Check against caller class
528 6 100       21 my $package = exists $opts->{_caller_package} ? $opts->{_caller_package} : (caller)[0];
529 6 100 66     34 return unless lc $DEF{DEBUG_CLASS} eq 'all' || $package eq $DEF{DEBUG_CLASS};
530              
531             # Check if we really want to debug syslog data
532 2         6 $opts->{syslog_level} = 'debug';
533 2   66     11 $opts->{no_syslog} //= !$DEF{SYSLOG_DEBUG};
534              
535             # Output
536 2         5 output( $opts, @msgs );
537             }
538              
539              
540             sub debug_var {
541 15     15 1 998 my $opts = {
542             clear => 1, # Meant for the screen
543             no_syslog => 1, # Meant for the screen
544             _caller_package => (caller)[0], # Make sure this is set on entry
545             };
546             # Merge with options
547 15 100 66     70 if( is_hashref($_[0]) && defined $_[1] && is_ref($_[1]) ) {
      66        
548 1         2 my $ref = shift;
549 1         2 foreach my $k (keys %{ $ref } ) {
  1         274  
550 1         4 $opts->{$k} = $ref->{$k};
551             };
552             }
553              
554 15         25 my $var = shift;
555 15 50 33     48 debug($opts, $DEF{ENCODE} eq 'json' || $opts->{json} ? $var : YAML::XS::Dump $var);
556             }
557              
558              
559             my %_allow_override = map { $_ => 1 } qw(debug verbose);
560             sub override {
561 0     0 1 0 my ($var,$value) = @_;
562              
563 0 0       0 return unless exists $_allow_override{lc $var};
564              
565 0         0 my $def_var = uc $var;
566 0         0 $DEF{$def_var} = $value;
567             }
568              
569              
570             my $_Confirm_Valid;
571             sub confirm {
572 0     0 1 0 my ($question) = @_;
573              
574             # Initialize Globals
575 0   0     0 $_Confirm_Valid ||= {qw(y 1 yes 1 n 0 no 0)};
576              
577 0         0 $question =~ s/\s*$/ [yN] /;
578 0         0 my $answer = undef;
579 0   0     0 until( defined $answer && exists $_Confirm_Valid->{$answer} ) {
580 0 0       0 output({color=>'red',stderr=>1},"ERROR: must be one of 'y','n','yes','no'") if defined $answer;
581 0         0 $answer = lc _get_input($question);
582             }
583 0         0 return $_Confirm_Valid->{$answer};
584             }
585              
586              
587             sub text_input {
588 0     0 1 0 my $question = shift;
589 0         0 my %args = @_;
590              
591             # Prompt fixes
592 0         0 chomp($question);
593 0 0       0 my $terminator = $question =~ s/([^a-zA-Z0-9\)\]\}])\s*$// ? $1 : ':';
594 0 0       0 if(exists $args{default}) {
595 0         0 $question .= " (default=$args{default}) ";
596             }
597 0         0 $question .= "$terminator ";
598              
599             # Make sure there's a space before the prompt
600 0         0 $question =~ s/\s*$/ /;
601 0 0       0 my $validate = exists $args{validate} ? $args{validate} : {};
602              
603 0         0 my $text;
604 0         0 my $error = undef;
605 0   0     0 until( defined $text && !defined $error ) {
606 0 0       0 output({color=>'red',stderr=>1},"ERROR: $error") if defined $error;
607              
608             # Try to have the user answer the question
609 0         0 $text = _get_input($question => \%args);
610 0         0 $error = undef;
611              
612             # Check the default if the person just hit enter
613 0 0 0     0 if( exists $args{default} && length($text) == 0 ) {
614 0         0 return $args{default};
615             }
616 0         0 foreach my $v (keys %{$validate}) {
  0         0  
617 0         0 local $_ = $text;
618 0 0       0 if( $validate->{$v}->() > 0 ) {
619 0         0 debug({indent=>1}," + Validated: $v");
620 0         0 next;
621             }
622 0         0 $error = $v;
623 0         0 last;
624             }
625             }
626 0         0 return $text;
627             }
628              
629              
630             sub menu {
631 0     0 1 0 my ($question,$opts) = @_;
632 0         0 my %desc = ();
633              
634             # Determine how to handle this list
635 0 0       0 if( is_arrayref($opts) ) {
    0          
636 0         0 %desc = map { $_ => $_ } @{ $opts };
  0         0  
  0         0  
637             }
638             elsif( is_hashref($opts) ) {
639 0         0 %desc = %{ $opts };
  0         0  
640             }
641              
642 0         0 print "$question\n\n";
643 0         0 my %ref = ();
644 0         0 my $id = 0;
645 0         0 foreach my $key (sort keys %desc) {
646 0         0 $ref{++$id} = $key;
647             }
648              
649 0         0 my $choice;
650 0   0     0 until( defined $choice && exists $ref{$choice} ) {
651 0 0       0 output({color=>'red',stderr=>1},"ERROR: invalid selection") if defined $choice;
652 0         0 foreach my $id (sort { $a <=> $b } keys %ref) {
  0         0  
653 0         0 printf " %d. %s\n", $id, $desc{$ref{$id}};
654             }
655 0         0 print "\n";
656 0         0 $choice = _get_input("Selection (1-$id): ");
657             }
658 0         0 return $ref{$choice};
659             }
660              
661              
662             sub pwprompt {
663 0     0 1 0 my ($prompt, %args) = @_;
664 0   0     0 $prompt ||= "Password: ";
665 0         0 my @more_validate;
666 0 0       0 if (my $validate = $args{validate}){
667 0         0 @more_validate = %$validate;
668             }
669             return text_input($prompt,
670             noecho => 1,
671             clear_line => 1,
672 0 0   0   0 validate => { "password length can't be zero." => sub { defined && length },
673 0         0 @more_validate,
674             },
675             );
676             }
677              
678              
679             sub prompt {
680 0     0 1 0 my ($prompt) = shift;
681 0         0 my %args = @_;
682              
683 0 0       0 return confirm($prompt) if exists $args{yn};
684 0 0       0 return menu($prompt, $args{menu}) if exists $args{menu};
685             # Check for a password prompt
686 0 0       0 if( lc($prompt) =~ /passw(or)?d/ ) {
687 0         0 $args{noecho} = 1;
688 0   0     0 $args{validate} ||= {};
689 0 0   0   0 $args{validate}->{"password length can't be zero."} = sub { defined && length };
  0         0  
690             }
691 0         0 return text_input($prompt,%args);
692             }
693              
694             sub _get_input {
695 0     0   0 my ($prompt,$args) = @_;
696              
697 0         0 state $interactive = is_interactive();
698 0         0 state $term;
699              
700 0         0 my $text = '';
701 0 0       0 if( $interactive ) {
702             # Initialize Term
703 0   0     0 $term ||= Term::ReadLine->new($0);
704 0   0     0 $args ||= {};
705 0 0       0 print "\e[s" if $args->{clear_line}; # Save cursor position
706 0 0       0 if( $args->{noecho} ) {
707             # Disable all the Term ReadLine magic
708 0         0 local $|=1;
709 0         0 print $prompt;
710 0         0 ReadMode('noecho');
711 0         0 $text = ReadLine();
712 0         0 ReadMode('restore');
713 0         0 print "\n";
714 0         0 chomp($text);
715             }
716             else {
717 0         0 $text = $term->readline($prompt);
718 0 0 0     0 $term->addhistory($text) if length $text && $text =~ /\S/;
719             }
720 0 0       0 print "\e[u\e[K" if $args->{clear_line}; # Return to saved position, erase line
721             }
722             else {
723             # Read one line from STDIN
724 0         0 $text = <>;
725             }
726 0         0 return $text;
727             }
728              
729              
730              
731             # Return True
732             1;
733              
734             __END__
735              
736             =pod
737              
738             =head1 NAME
739              
740             CLI::Helpers - Subroutines for making simple command line scripts
741              
742             =head1 VERSION
743              
744             version 2.2
745              
746             =head1 SYNOPSIS
747              
748             This module provides a library of useful functions for constructing simple command
749             line interfaces. It is able to extract information from the environment and your
750             ~/.gitconfig to display data in a reasonable manner.
751              
752             use CLI::Helpers;
753              
754             ...
755             output({color=>"green"}, "Hello, world!");
756             debug({color=>"yellow"}, "Debug output!");
757              
758             Using this module adds argument parsing using L<Getopt::Long> to your script. It
759             enables pass-through, so you can still use your own argument parsing routines or
760             L<Getopt::Long> in your script.
761              
762             =head1 EXPORT
763              
764             This module exports the C<:all> group by default.
765              
766             =head2 Export Groups
767              
768             Optionally, you can specify the groups you prefer:
769              
770             =over 2
771              
772             =item B<:output>
773              
774             output()
775             verbose()
776             debug()
777             debug_var()
778              
779             =item B<:input>
780              
781             menu()
782             text_input()
783             confirm()
784             prompt()
785             pwprompt()
786              
787             =item B<:all>
788              
789             :output
790             :input
791              
792             =back
793              
794             All groups include these functions:
795              
796             cli_helpers_initialize()
797             options_description()
798              
799             =head2 Configuration
800              
801             It's possible to change the behavior of the import process.
802              
803             =over 2
804              
805             =item B<copy_argv>
806              
807             Instead of messing with C<@ARGV>, operate on a copy of C<@ARGV>.
808              
809             use CLI::Helpers qw( :output copy_argv );
810              
811             =item B<preprocess_argv>
812              
813             This causes the C<@ARGV> processing to happen during the C<INIT> phase, after
814             import but before runtime. This is usually OK for scripts, but for use in
815             libraries, it may be undesirable. This is the default when C<CLI::Helpers> is
816             imported from the C<main> package.
817              
818             use CLI::Helpers qw( :output preprocess_argv );
819              
820             =item B<delay_argv>
821              
822             This causes the C<@ARGV> processing to happen when the first call to a function
823             needing it run, usually an C<output()> call. This is the default when import
824             from any other package other than C<main>.
825              
826             use CLI::Helpers qw( :output delay_argv );
827              
828             =back
829              
830             =head1 OUTPUT FUNCTIONS
831              
832             =head2 output( \%opts, @messages )
833              
834             Exported. Takes an optional hash reference and a list of
835             messages to be output.
836              
837             Any data references in the the C<@messages> list will automatically be converted to JSON. You may also pass
838              
839             { yaml => 1 }
840              
841             In the options to output in YAML format.
842              
843             =head2 verbose( \%opts, @messages )
844              
845             Exported. Takes an optional hash reference of formatting options. Automatically
846             overrides the B<level> parameter to 1 if it's not set.
847              
848             =head2 debug( \%opts, @messages )
849              
850             Exported. Takes an optional hash reference of formatting options.
851             Does not output anything unless DEBUG is set.
852              
853             =head2 debug_var( \%opts, \%Variable )
854              
855             Exported. Takes an optional hash reference of formatting options.
856             Does not output anything unless DEBUG is set.
857              
858             Passing:
859              
860             { json => 1 }
861              
862             in C<\%opts> will format the output as JSON
863              
864             =head1 OUTPUT OPTIONS
865              
866             Every output function takes an optional HASH reference containing options for
867             that output. The hash may contain the following options:
868              
869             =over 4
870              
871             =item B<tag>
872              
873             Add a keyword to tag output with. The user may then specify C<--tags
874             keyword1,keyword2> to only view output at the appropriate level. This option
875             will affect C<data-file> and C<syslog> output. The output filter requires both
876             the presence of the C<tag> in the output options B<and> the user to specify
877             C<--tags> on the command line.
878              
879             Consider a script, C<status.pl>:
880              
881             output("System Status: Normal")
882             output({tag=>'foo'}, "Component Foo: OK");
883             output({tag=>'bar'}, "Component Bar: OK");
884              
885             If an operator runs:
886              
887             $ status.pl
888             System Status: Normal
889             Component Foo: OK
890             Component Bar: OK
891              
892             $ status.pl --tags bar
893             System Status: Normal
894             Component Bar: OK
895              
896             $ status.pl --tags foo
897             System Status: Normal
898             Component Foo: OK
899              
900             This could be helpful for selecting one or more pertinent tags to display.
901              
902             =item B<sticky>
903              
904             Any lines tagged with 'sticky' will be replayed at the end program's end. This
905             is to allow a developer to ensure message are seen at the termination of the program.
906              
907             =item B<color>
908              
909             String. Using Term::ANSIColor for output, use the color designated, i.e.:
910              
911             red,blue,green,yellow,cyan,magenta,white,black, etc..
912              
913             =item B<level>
914              
915             Integer. For verbose output, this is basically the number of -v's necessary to see
916             this output.
917              
918             =item B<syslog_level>
919              
920             String. Can be any valid syslog_level as a string: debug, info, notice, warning, err, crit,
921             alert, emerg.
922              
923             =item B<no_syslog>
924              
925             Bool. Even if the user specifies --syslog, these lines will not go to the syslog destination.
926             alert, emerg.
927              
928             =item B<IMPORTANT>
929              
930             Bool. Even if --quiet is specified, output this message. Use sparingly, and yes,
931             it is case sensitive. You need to yell at it for it to yell at your users.
932              
933             =item B<stderr>
934              
935             Bool. Use STDERR for this message instead of STDOUT. The advantage to using this is the
936             "quiet" option will silence these messages as well.
937              
938             =item B<indent>
939              
940             Integer. This will indent by 2 times the specified integer the next string. Useful
941             for creating nested output in a script.
942              
943             =item B<clear>
944              
945             Integer. The number of newlines before this output.
946              
947             =item B<kv>
948              
949             Bool. The array of messages is actually a key/value pair, this implements special coloring and
950             expects the number of messages to be even.
951              
952             output(qw(a 1 b 2));
953             # a
954             # 1
955             # b
956             # 2
957              
958             Using kv, the output will look like this:
959              
960             output({kv=>1}, qw(a 1 b 2));
961             # a: 1
962             # b: 2
963             #
964              
965             =item B<data>
966              
967             Bool. Lines tagged with "data => 1" will be output to the data-file if a user specifies it. This allows
968             you to provide header/footers and inline context for the main CLI, but output just the data to a file for
969             piping elsewhere.
970              
971             =back
972              
973             =head1 INPUT FUNCTIONS
974              
975             =head2 confirm("prompt")
976              
977             Exported. Creates a Yes/No Prompt which accepts y/n or yes/no case insensitively
978             but requires one or the other.
979              
980             Returns 1 for 'yes' and 0 for 'no'
981              
982             =head2 text_input("prompt", validate => { "too short" => sub { length $_ > 10 } })
983              
984             Exported. Provides a prompt to the user for input. If validate is passed, it should be a hash reference
985             containing keys of error messages and values which are subroutines to validate the input available as $_.
986             If a validator fails, it's error message will be displayed, and the user will be re-prompted.
987              
988             Valid options are:
989              
990             =over 4
991              
992             =item B<default>
993              
994             Any string which will be used as the default value if the user just presses enter.
995              
996             =item B<validate>
997              
998             A hashref, keys are error messages, values are sub routines that return true when the value meets the criteria.
999              
1000             =item B<noecho>
1001              
1002             Set as a key with any value and the prompt will turn off echoing responses as well as disabling all
1003             ReadLine magic. See also B<pwprompt>.
1004              
1005             =item B<clear_line>
1006              
1007             After prompting for and receiving input, remove that line from the terminal.
1008             This only works on interactive terminals, and is useful for things like
1009             password prompts.
1010              
1011             =back
1012              
1013             Returns the text that has passed all validators.
1014              
1015             =head2 menu("prompt", $ArrayOrHashRef)
1016              
1017             Exported. Used to create a menu of options from a list. Can be either a hash or array reference
1018             as the second argument. In the case of a hash reference, the values will be displayed as options while
1019             the selected key is returned. In the case of an array reference, each element in the list is displayed
1020             the selected element will be returned.
1021              
1022             Returns selected element (HashRef -> Key, ArrayRef -> The Element)
1023              
1024             =head2 pwprompt("Prompt", options )
1025              
1026             Exported. Synonym for text_input("Password: ", noecho => 1); Also requires the password to be longer than 0 characters.
1027              
1028             =head2 prompt("Prompt", options )
1029              
1030             Exported. Wrapper function with rudimentary mimicry of IO::Prompt(er).
1031             Uses:
1032              
1033             # Mapping back to confirm();
1034             my $value = prompt "Are you sure?", yn => 1;
1035              
1036             # Mapping back to text_input();
1037             my $value = prompt "Enter something:";
1038              
1039             # With Validator
1040             my $value = prompt "Enter an integer:", validate => { "not a number" => sub { /^\d+$/ } }
1041              
1042             # Pass to menu();
1043             my $value = prompt "Select your favorite animal:", menu => [qw(dog cat pig fish otter)];
1044              
1045             # If you request a password, autodisable echo:
1046             my $passwd = prompt "Password: "; # sets noecho => 1, disables ReadLine history.
1047              
1048             See also: B<text_input>
1049              
1050             =head1 OTHER FUNCTIONS
1051              
1052             =head2 cli_helpers_initialize()
1053              
1054             This is called automatically when C<preprocess_argv> is set. By default, it'll
1055             be run the first time a definition is needed, usually the first call to
1056             C<output()>. If called automatically, it will operate on C<@ARGV>. You can
1057             optionally pass an array reference to this function and it'll operate that
1058             instead.
1059              
1060             In most cases, you don't need to call this function directly.
1061              
1062             #!perl
1063             # Normal use from main package in a script
1064             use v5.42;
1065             use CLI::Helpers qw( :output );
1066              
1067             ...
1068             # CLI::Helpers has already stripped its args from @ARGV
1069             my %opts = get_important_things_from(\@ARGV);
1070              
1071             output("ready");
1072              
1073             This is the same as specifying C<preprocess_argv> from the C<main> package.
1074              
1075             use CLI::Helpers qw( :output preprocess_argv );
1076              
1077             ...
1078             # Since CLI::Helpers opts are stripped from @ARGV,
1079             # Getopt::Long::Descriptive won't complain about extra args
1080             my ($opt,$usage) = describe_option( ... );
1081              
1082             # Now, let CLI::Helpers take the rest, implicit
1083             # call to cli_helpers_initialize()
1084             output("ready");
1085              
1086             Or if you'd prefer not to touch C<@ARGV> at all, you pass in an array ref:
1087              
1088             use CLI::Helpers qw( :output delay_argv );
1089              
1090             my ($opt,$usage) = describe_option( ... );
1091              
1092             cli_helpers_initialize([ qw( --verbose ) ]);
1093              
1094             output("ready?");
1095             verbose("you bet I am");
1096              
1097             =head2 options_description()
1098              
1099             Returns an array of arrayrefs to use with L<Getopt::Long::Descriptive>.
1100              
1101             use CLI::Helpers qw( option_description );
1102             use Getopt::Long::Descriptive qw( describe_options );
1103              
1104             my ($opt,$usage) = describe_options("%c %o",
1105             # Your opts here
1106             options_description(),
1107             );
1108              
1109             =head2 colorize( $color => 'message to be output' )
1110              
1111             Not exported by default. Checks if color is enabled and applies
1112             the specified color to the string.
1113              
1114             =head2 git_color_check()
1115              
1116             Not exported by default. Returns 1 if git is configured to output
1117             using color of 0 if color is not enabled.
1118              
1119             =head2 def($key)
1120              
1121             Not exported by default, returns the setting defined.
1122              
1123             =head2 override( variable => 1 )
1124              
1125             Exported. Allows a block of code to override the debug or verbose level. This
1126             can be used during development to enable/disable the DEBUG/VERBOSE settings.
1127              
1128             =head1 ARGS
1129              
1130             From CLI::Helpers:
1131              
1132             --data-file Path to a file to write lines tagged with 'data => 1'
1133             --tags A comma separated list of tags to display
1134             --color Boolean, enable/disable color, default use git settings
1135             --verbose Incremental, increase verbosity (Alias is -v)
1136             --debug Show developer output
1137             --debug-class Show debug messages originating from a specific package, default: main
1138             --quiet Show no output (for cron)
1139             --syslog Generate messages to syslog as well
1140             --syslog-facility Default "local0"
1141             --syslog-tag The program name, default is the script name
1142             --syslog-debug Enable debug messages to syslog if in use, default false
1143             --nopaste Use App::Nopaste to paste output to configured paste service
1144             --nopaste-public Defaults to false, specify to use public paste services
1145             --nopaste-service Comma-separated App::Nopaste service, defaults to Shadowcat
1146              
1147             =head1 EXAMPLE
1148              
1149             A simple example of how to use this module and what it does.
1150              
1151             =head2 Script
1152              
1153             #!/usr/bin/env perl
1154             use CLI::Helpers qw(:all);
1155              
1156             output({color=>'green'}, "Hello, World!");
1157             verbose({indent=>1,color=>'yellow'}, "Shiny, happy people!");
1158             verbose({level=>2,kv=>1,color=>'red'}, a => 1, b => 2);
1159             debug_var({ c => 3, d => 4});
1160              
1161             # Data
1162             output({data=>1}, join(',', qw(a b c d)));
1163              
1164             # Wait for confirmation
1165             die "ABORTING" unless confirm("Are you sure?");
1166              
1167             # Ask for a number
1168             my $integer = prompt "Enter an integer:", validate => { "not a number" => sub { /^\d+$/ } }
1169              
1170             # Ask for next move
1171             my %menu = (
1172             north => "Go north.",
1173             south => "Go south.",
1174             );
1175             my $dir = prompt "Where to, adventurous explorer?", menu => \%menu;
1176              
1177             # Ask for a favorite animal
1178             my $favorite = menu("Select your favorite animal:", [qw(dog cat pig fish otter)]);
1179              
1180             =head2 Usage
1181              
1182             $ ./test.pl
1183             Hello, World!
1184             a,b,c,d
1185             $ ./test.pl --verbose
1186             Hello, World!
1187             Shiny, Happy people!
1188             a,b,c,d
1189             $ ./test.pl -vv
1190             Hello, World!
1191             Shiny, Happy people!
1192             a: 1
1193             b: 2
1194             a,b,c,d
1195             $ ./test.pl --debug
1196             Hello, World!
1197             Shiny, Happy people!
1198             a: 1
1199             b: 2
1200             ---
1201             c: 3
1202             d: 4
1203             a,b,c,d
1204              
1205             $ ./test.pl --data-file=output.csv
1206             Hello, World!
1207             a,b,c,d
1208             $ cat output.csv
1209             a,b,c,d
1210              
1211             Colors would be automatically enabled based on the user's ~/.gitconfig
1212              
1213             =head1 NOPASTE
1214              
1215             This is optional and will only work if you have L<App::Nopaste> installed. If
1216             you just specify C<--nopaste>, any output that would be displayed to the screen
1217             is submitted to the L<App::Nopaste::Service::Shadowcat> paste bin. This
1218             paste service is pretty simple, but works reliably.
1219              
1220             During the C<END> block, the output is submitted and the URL of the paste is
1221             returned to the user.
1222              
1223             =head1 AUTHOR
1224              
1225             Brad Lhotsky <brad@divisionbyzero.net>
1226              
1227             =head1 CONTRIBUTORS
1228              
1229             =for stopwords Kang-min Liu Kevin M. Goess Mohammad S Anwar
1230              
1231             =over 4
1232              
1233             =item *
1234              
1235             Kang-min Liu <gugod@gugod.org>
1236              
1237             =item *
1238              
1239             Kevin M. Goess <kgoess@craigslist.org>
1240              
1241             =item *
1242              
1243             Mohammad S Anwar <mohammad.anwar@yahoo.com>
1244              
1245             =back
1246              
1247             =for :stopwords cpan testmatrix url bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
1248              
1249             =head1 SUPPORT
1250              
1251             =head2 Websites
1252              
1253             The following websites have more information about this module, and may be of help to you. As always,
1254             in addition to those websites please use your favorite search engine to discover more resources.
1255              
1256             =over 4
1257              
1258             =item *
1259              
1260             MetaCPAN
1261              
1262             A modern, open-source CPAN search engine, useful to view POD in HTML format.
1263              
1264             L<https://metacpan.org/release/CLI-Helpers>
1265              
1266             =item *
1267              
1268             CPAN Testers
1269              
1270             The CPAN Testers is a network of smoke testers who run automated tests on uploaded CPAN distributions.
1271              
1272             L<http://www.cpantesters.org/distro/C/CLI-Helpers>
1273              
1274             =item *
1275              
1276             CPAN Testers Matrix
1277              
1278             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
1279              
1280             L<http://matrix.cpantesters.org/?dist=CLI-Helpers>
1281              
1282             =back
1283              
1284             =head2 Bugs / Feature Requests
1285              
1286             This module uses the GitHub Issue Tracker: L<https://github.com/reyjrar/CLI-Helpers/issues>
1287              
1288             =head2 Source Code
1289              
1290             This module's source code is available by visiting:
1291             L<https://github.com/reyjrar/CLI-Helpers>
1292              
1293             =cut