File Coverage

blib/lib/MooseX/Getopt/Usage/Formatter.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package MooseX::Getopt::Usage::Formatter;
2              
3 1     1   2978 use 5.010;
  1         3  
  1         45  
4             our $VERSION = '0.24';
5              
6 1     1   227 use Moose;
  0            
  0            
7             #use MooseX::StrictConstructor;
8             use Moose::Util::TypeConstraints;
9             use Term::ANSIColor;
10             use Term::ReadKey;
11             use Text::Wrap;
12             use Pod::Usage;
13             use Pod::Select;
14             use Pod::Find qw(pod_where contains_pod);
15             use MooseX::Getopt::Usage::Pod::Text;
16             use File::Basename;
17             use Module::Loaded;
18             use FindBin;
19              
20             BEGIN {
21             # Grab prog name before someone decides to change it.
22             my $prog_name;
23             sub prog_name { @_ ? ($prog_name = shift) : $prog_name }
24             prog_name(File::Basename::basename($0));
25             }
26              
27             # Util wrapper for pod select and its file based API
28             sub podselect_text {
29             my @args = @_;
30             my $selected = "";
31             open my $fh, ">", \$selected or die;
32             if ( exists $args[0] and ref $args[0] eq "HASH" ) {
33             $args[0]->{'-output'} = $fh;
34             }
35             else {
36             unshift @args, { '-output' => $fh };
37             }
38             podselect @args;
39             return $selected;
40             }
41              
42             #
43             # Types
44              
45             subtype 'PodSelectList', as 'ArrayRef[Str]';
46              
47             enum 'ColorUsage', [qw(auto never always env)];
48              
49              
50             #
51             # Attributes
52              
53             has getopt_class => (
54             is => "rw",
55             isa => "ClassName",
56             required => 1,
57             );
58              
59             has pod_file => (
60             is => "rw",
61             isa => "Undef|Str",
62             lazy_build => 1,
63             );
64              
65             sub _build_pod_file {
66             my $self = shift;
67              
68             # Script file, may have inline pod docs
69             my $file = "$FindBin::Bin/$FindBin::Script";
70             return $file if -f $file && contains_pod($file);
71              
72             # Use the pod docs from the class
73             my $gclass = $self->getopt_class;
74             return pod_where( {-inc => 1}, $gclass ) if is_loaded($gclass);
75              
76             return undef;
77             }
78              
79             has colours => (
80             is => "rw",
81             isa => "HashRef",
82             default => sub { {
83             flag => ['yellow'],
84             heading => ['bold'],
85             command => ['green'],
86             type => ['magenta'],
87             default_value => ['cyan'],
88             error => ['red']
89             } },
90             );
91              
92             has headings => (
93             is => "rw",
94             isa => "Bool",
95             default => 1,
96             );
97              
98             has groups => (
99             is => "rw",
100             isa => "Undef|Bool",
101             default => undef,
102             );
103              
104             has format => (
105             is => "rw",
106             isa => "Str",
107             lazy_build => 1,
108             );
109              
110             sub _build_format {
111             my $self = shift;
112             my $pod_file = $self->pod_file;
113             my $sections = $self->format_sections;
114             my $selected = "";
115             if ( $pod_file ) {
116             $selected = podselect_text { -sections => $sections }, $pod_file;
117             $selected =~ s{^=head1.*?\n$}{}mg;
118             $selected =~ s{^.*?\n}{};
119             $selected =~ s{\n$}{};
120             }
121             return $selected ? $selected : "%c [OPTIONS]";
122             }
123              
124             has width => (
125             is => "rw",
126             isa => "Int",
127             lazy_build => 1,
128             );
129              
130             sub _build_width {
131             my $self = shift;
132             my $w = 72;
133             if (-t STDOUT) {
134             my ($tw) = GetTerminalSize();
135             $w = $tw -1 if defined $tw;
136             }
137             return $w;
138             }
139              
140              
141             has attr_sort => (
142             is => "rw",
143             isa => "CodeRef",
144             default => sub { sub {0} },
145             );
146              
147             has use_color => (
148             is => "rw",
149             isa => "ColorUsage",
150             default => "auto",
151             );
152              
153             has format_sections => (
154             is => "rw",
155             isa => "PodSelectList",
156             default => sub { ["SYNOPSIS"] },
157             );
158              
159             has usage_sections => (
160             is => "rw",
161             isa => "PodSelectList",
162             default => sub { ["SYNOPSIS|OPTIONS"] },
163             );
164              
165             has man_sections => (
166             is => "rw",
167             isa => "PodSelectList",
168             default => sub { ["!ATTRIBUTES|METHODS"] },
169             );
170              
171             has unexpand => (
172             is => "rw",
173             isa => "Int",
174             default => 0,
175             );
176              
177             has tabstop => (
178             is => "rw",
179             isa => "Int",
180             default => 4,
181             );
182              
183             #
184             # Methods
185              
186             sub _set_color_handling {
187             my $self = shift;
188             my $mode = shift;
189              
190             $ENV{ANSI_COLORS_DISABLED} = defined $ENV{ANSI_COLORS_DISABLED} ? 1 : undef;
191             if ($mode eq 'auto') {
192             if ( not defined $ENV{ANSI_COLORS_DISABLED} ) {
193             $ENV{ANSI_COLORS_DISABLED} = -t STDOUT ? undef : 1;
194             }
195             }
196             elsif ($mode eq 'always') {
197             $ENV{ANSI_COLORS_DISABLED} = undef;
198             }
199             elsif ($mode eq 'never') {
200             $ENV{ANSI_COLORS_DISABLED} = 1;
201             }
202             # 'env' is done in the env set line above
203             }
204              
205             sub usage {
206             my $self = shift;
207             my $args = { @_ };
208              
209             my $exit = $args->{exit};
210             my $err = $args->{err} || "";
211             my $colours = $self->colours;
212              
213             # Set the color handling for this call
214             $self->_set_color_handling( $args->{use_color} || $self->use_color );
215              
216             my $pod = $self->_get_pod(
217             sections => $self->usage_sections,
218             options_style => 'text',
219             );
220             my $parser = MooseX::Getopt::Usage::Pod::Text->new(
221             width => $self->width,
222             headings => $self->headings
223             );
224             my $out;
225             $parser->output_string(\$out);
226             $parser->parse_string_document($pod);
227              
228             $out = colored($colours->{error}, $err)."\n".$out if $err;
229              
230             if ( defined $exit ) {
231             print $out;
232             exit $exit;
233             }
234             return $out;
235             }
236              
237             sub manpage {
238             my $self = shift;
239              
240             $self->_set_color_handling('never');
241              
242             my $pod = $self->_get_pod( sections => $self->man_sections );
243              
244             open my $fh, "<", \$pod or die;
245             pod2usage( -verbose => 2, -input => $fh );
246             }
247              
248             # Get the pod for the target class. Fills in missing sections.
249             sub _get_pod {
250             my $self = shift;
251             my %args = @_;
252             my $opt_style = $args{options_style} || "pod";
253             my $sections = $args{sections} || [];
254             my $gclass = $self->getopt_class;
255              
256             # Grab all the pod text (strips out the code).
257             my $pod = $self->pod_file ? podselect_text( $self->pod_file ) : "";
258              
259             # XXX Some dirty pod regexp hacking. Needs moving to a real parser.
260             # Insert SYNOPSIS if not there. After NAME or top of pod.
261             unless ($pod =~ m/^=head1\s+SYNOPSIS\s*$/ms) {
262             my $synopsis = "\n=head1 SYNOPSIS\n\n".$self->format."\n";
263             if ($pod =~ m/^=head1\s+NAME\s*$/ms) {
264             $pod =~ s/(^=head1\s+NAME\s*\n.*?)(^=|\z)/$1$synopsis\n\n$2/ms;
265             }
266             else {
267             $pod = "$synopsis\n$pod";
268             }
269             }
270              
271             # Insert OPTIONS if not there. After DESCRIPTION or SYNOPSIS or end of pod.
272             unless ($pod =~ m/^=head1\s+OPTIONS\s*$/ms) {
273             my $newpod = "\n=head1 OPTIONS\n\n";
274             if ($pod =~ m/^=head1\s+DESCRIPTION\s*$/ms) {
275             $pod =~ s/(^=head1\s+DESCRIPTION\s*\n.*?)(^=|\z)/$1$newpod$2/ms;
276             }
277             elsif ($pod =~ m/^=head1\s+SYNOPSIS\s*$/ms) {
278             $pod =~ s/(^=head1\s+SYNOPSIS\s*\n.*?)(^=|\z)/$1$newpod$2/ms;
279             }
280             else {
281             $pod = "$pod\n$newpod";
282             }
283             }
284              
285             # Add options list to OPTIONS
286             my $meth = "_options_$opt_style";
287             my $options = $self->$meth;
288             $pod =~ s/(^=head1\s+OPTIONS\s*\n.*?)
289             (^=|\z)
290             /$1\n$options$2/msx;
291              
292             # Process the SYNOPSIS
293             $pod =~ s/(^=head1\s+SYNOPSIS\s*\n) # The header $1
294             (.*?) # Content $2
295             (^=|\z) # Next section or eof $3
296             /$1.$self->_parse_format($2).$3/mesx;
297              
298             # Select again to trim down to just the sections asked for.
299             my $out = "";
300             open my $fhin, "<", \$pod or die;
301             open my $fhout, ">", \$out or die;
302             my $selector = Pod::Select->new();
303             $selector->select(@$sections);
304             $selector->parse_from_filehandle($fhin, $fhout);
305             return $out;
306             }
307              
308             # Return list of class attributes that are options.
309             sub _getopt_attrs {
310             my $self = shift;
311             my $gclass = $self->getopt_class;
312             my $attr_sort = $self->attr_sort;
313             return sort { $attr_sort->($a, $b) } $gclass->_compute_getopt_attrs;
314             }
315              
316             # Generate POD version of the options from the meta info.
317             sub _options_pod {
318             my $self = shift;
319              
320             my @attrs = $self->_getopt_attrs;
321             my $options_pod = "";
322             $options_pod .= "=over 4\n\n";
323             foreach my $attr (@attrs) {
324             my $label = $self->_attr_label($attr);
325             $options_pod .= "=item B<$label>\n\n";
326             $options_pod .= ($attr->documentation || "")."\n\n";
327             }
328             $options_pod .= "=back\n\n";
329             return $options_pod;
330             }
331              
332             # Generate (colored) text version of the options from meta info.
333             sub _options_text {
334             my $self = shift;
335             my $args = { @_ };
336             my $colours = $self->colours;
337              
338             my @attrs = $self->_getopt_attrs;
339             my $max_len = 0;
340             my (@req_attrs, @opt_attrs);
341             foreach (@attrs) {
342             my $len = length($self->_attr_label($_));
343             $max_len = $len if $len > $max_len;
344             if ( $_->is_required && !$_->has_default && !$_->has_builder ) {
345             push @req_attrs, $_;
346             }
347             else {
348             push @opt_attrs, $_;
349             }
350             }
351              
352             my $groups = $self->groups;
353             $groups = @req_attrs ? 1 : 0 if not defined $groups;
354             my $indent = $groups ? 4 : 0;
355              
356             my $out = " ";
357             $out .= colored($colours->{heading}, "Required:")."\n"
358             if $groups && @req_attrs;
359             $out .= $self->_attr_str($_, max_len => $max_len, indent => $indent )."\n"
360             foreach @req_attrs;
361             $out .= colored($colours->{heading}, "Optional:")."\n"
362             if $groups && @opt_attrs;
363             $out .= $self->_attr_str($_, max_len => $max_len, indent => $indent )."\n"
364             foreach @opt_attrs;
365             $out =~ s{\n}{\n }gsm; # Make into pod preformat para
366             $out .= "\n\n";
367              
368             return $out;
369             }
370              
371             sub _parse_format {
372             my $self = shift;
373             my $fmt = shift or confess "No format";
374             my $colours = $self->colours;
375              
376             $fmt =~ s/%c/colored $colours->{command}, prog_name()/ieg;
377             $fmt =~ s/%a/$self->_format_opt_line('a')/ieg;
378             $fmt =~ s/%r/$self->_format_opt_line('r')/ieg;
379             $fmt =~ s/%o/$self->_format_opt_line('o')/ieg;
380             $fmt =~ s/%%/%/g;
381             # TODO - Be good to have a include that generates a list of the opts
382             # %r - required %a - all %o - options
383             $fmt =~ s/^(.*?:\n)/colored $colours->{heading}, "$1"/egm;
384             $self->_colourise(\$fmt);
385             return $fmt;
386             }
387              
388             sub _format_opt_line {
389             my $self = shift;
390             my $group = shift;
391              
392             my @attrs;
393             if ( !$group || $group eq "a" ) {
394             @attrs = $self->_getopt_attrs;
395             }
396             elsif ( $group eq "r" ) {
397             @attrs = grep {
398             $_->is_required && !$_->has_default && !$_->has_builder
399             } $self->_getopt_attrs;
400             }
401             elsif ( $group eq "o" ) {
402             @attrs = grep {
403             !($_->is_required && !$_->has_default && !$_->has_builder)
404             } $self->_getopt_attrs;
405             }
406             else {
407             confess "Unknown grouping: $group";
408             }
409              
410             my @out;
411             foreach my $attr (@attrs) {
412             my $opt = "";
413             my $label = $self->_attr_label($attr);
414             $opt .= "$label";
415             if ( not $attr->type_constraint->is_a_type_of("Bool") ) {
416             $opt .= "=".uc($attr->name)
417             }
418             if (!$attr->is_required || $attr->has_default || $attr->has_builder) {
419             $opt = "[$opt]";
420             }
421             push @out, $opt;
422             }
423             return join(" ", @out);;
424             }
425              
426             # Return the full label, including aliases and dashes, for the passed attribute
427             sub _attr_label {
428             my $self = shift;
429             my $attr = shift || confess "No attr";
430             my $gclass = $self->getopt_class;
431              
432             my ( $flag, @aliases ) = $gclass->_get_cmd_flags_for_attr($attr);
433             my $label = join " ", map {
434             length($_) == 1 ? "-$_" : "--$_"
435             } ($flag, @aliases);
436             return $label;
437             }
438              
439             # Return the formated and coloured usage string for the passed attribute.
440             sub _attr_str {
441             my $self = shift;
442             my $attr = shift or confess "No attr";
443             my %args = @_;
444             my $max_len = $args{max_len} or confess "No max_len";
445             my $indent = $args{indent} || 0;
446             my $colours = $self->colours;
447              
448             local $Text::Wrap::columns = $self->width;
449             local $Text::Wrap::unexpand = $self->unexpand;
450             local $Text::Wrap::tabstop = $self->tabstop;
451              
452             my $label = $self->_attr_label($attr);
453              
454             my $docs = "";
455             my $pad = $max_len - length($label);
456             my $def = $attr->has_default ? $attr->default : undef;
457             (my $type = $attr->type_constraint) =~ s/(\w+::)*//g;
458             $docs .= colored($colours->{type}, "$type. ") if $type;
459             $docs .= colored($colours->{default_value}, "Default=$def").". "
460             if defined $def && ! ref $def;
461             $docs .= $attr->documentation || "";
462              
463             my $col1 = (" " x $indent).$label;
464             $col1 .= "".( " " x $pad );
465             my $out = wrap($col1, (" " x ($max_len + 9)), " - $docs" );
466             $self->_colourise(\$out);
467             return $out;
468             }
469              
470             # Extra colourisation for the attributes usage string. Think syntax highlight.
471             sub _colourise {
472             my $self = shift;
473             my $out = shift || "";
474             my $colours = $self->colours;
475              
476             my $str = ref $out ? $out : \$out;
477             $$str =~ s/(^|\s|\[)(--?[\w?]+)/"$1".colored $colours->{flag},"$2"/ge;
478             return ref $out ? $out : $$str;
479             }
480              
481              
482             __PACKAGE__->meta->make_immutable;
483             no Moose;
484              
485             1;
486             __END__