File Coverage

blib/lib/App/RecordStream/Operation.pm
Criterion Covered Total %
statement 131 282 46.4
branch 13 60 21.6
condition 13 31 41.9
subroutine 34 55 61.8
pod 0 37 0.0
total 191 465 41.0


line stmt bran cond sub pod time code
1             package App::RecordStream::Operation;
2              
3             our $VERSION = "4.0.25";
4              
5 69     69   46064 use strict;
  69         122  
  69         1512  
6 69     69   271 use warnings;
  69         104  
  69         1415  
7              
8 69     69   277 use Carp;
  69         97  
  69         3464  
9 69     69   27114 use FindBin qw($Script $RealScript);
  69         57488  
  69         7910  
10 69     69   40280 use Getopt::Long;
  69         586864  
  69         299  
11 69     69   39262 use Text::Autoformat;
  69         2157042  
  69         4253  
12              
13 69     69   23034 use App::RecordStream::Clumper;
  69         169  
  69         1525  
14 69     69   22846 use App::RecordStream::DomainLanguage;
  69         288  
  69         1692  
15 69     69   14801 use App::RecordStream::Executor;
  69         136  
  69         1402  
16 69     69   6417 use App::RecordStream::KeyGroups;
  69         709  
  69         1309  
17 69     69   283 use App::RecordStream::Site;
  69         102  
  69         980  
18 69     69   21911 use App::RecordStream::Stream::Base;
  69         144  
  69         1506  
19 69     69   21502 use App::RecordStream::Stream::Printer;
  69         140  
  69         1758  
20              
21 69     69   321 use base 'App::RecordStream::Stream::Base';
  69         114  
  69         155354  
22              
23             sub usage {
24 0     0 0 0 subclass_should_implement(shift);
25             }
26              
27             sub new {
28 168     168 0 349 my $class = shift;
29 168         235 my $args = shift;
30 168         214 my $next = shift;
31              
32 168         347 my $this = {
33             NEXT => $next,
34             };
35              
36 168         271 bless $this, $class;
37              
38 168         499 $this->init_help();
39 168         462 $this->init($args);
40 168         505 return $this;
41             }
42              
43             sub init_help {
44 168     168 0 245 my $this = shift;
45 168         2669 $this->{'HELP_TYPES'} = {
46             all => {
47             USE => 0,
48             SKIP_IN_ALL => 1,
49             CODE => \&all_help,
50             DESCRIPTION => 'Output all help for this script',
51             },
52             snippet => {
53             USE => 0,
54             SKIP_IN_ALL => 0,
55             CODE => \&snippet_help,
56             DESCRIPTION => 'Help on code snippets',
57             },
58             keygroups => {
59             USE => 0,
60             SKIP_IN_ALL => 0,
61             CODE => \&keygroups_help,
62             DESCRIPTION => 'Help on keygroups, a way of specifying multiple keys',
63             },
64             keyspecs => {
65             USE => 0,
66             SKIP_IN_ALL => 0,
67             CODE => \&keyspecs_help,
68             DESCRIPTION => 'Help on keyspecs, a way to index deeply and with regexes',
69             },
70             basic => {
71             USE => 1,
72             SKIP_IN_ALL => 0,
73             CODE => \&basic_help,
74             OPTION_NAME => 'help',
75             DESCRIPTION => 'This help screen',
76             },
77             'keys' => {
78             USE => 0,
79             SKIP_IN_ALL => 1,
80             CODE => \&keys_help,
81             DESCRIPTION => 'Help on keygroups and keyspecs',
82             },
83             domainlanguage => {
84             USE => 0,
85             SKIP_IN_ALL => 0,
86             CODE => \&domainlanguage_help,
87             DESCRIPTION => 'Help on the recs domain language, a [very complicated] way of specifying valuations (which act like keys) or aggregators',
88             },
89             clumping => {
90             USE => 0,
91             SKIP_IN_ALL => 0,
92             CODE => \&clumping_help,
93             DESCRIPTION => 'Help on clumping; mechanisms to group records across a stream'
94             },
95             };
96              
97 168         531 $this->add_help_types();
98             }
99              
100             # Prints out unified options for usage. $options is a hash with keys of the
101             # argument spec and values of the description
102             sub options_string {
103 0     0 0 0 my ($this, $options) = @_;
104              
105 0 0       0 push @$options, ['filename-key|fk ', 'Add a key with the source filename (if no filename is applicable will put NONE)']
106             if $this->does_record_output;
107              
108 0         0 my $string = $this->_options_format($options);
109 0         0 $string .= "\n Help Options:\n";
110              
111 0         0 my $help_options = [];
112 0         0 foreach my $type (sort keys %{$this->{'HELP_TYPES'}}) {
  0         0  
113 0         0 my $info = $this->{'HELP_TYPES'}->{$type};
114 0 0       0 next unless ( $info->{'USE'} );
115              
116 0   0     0 my $option_name = $info->{'OPTION_NAME'} || "help-$type";
117 0         0 my $description = $info->{'DESCRIPTION'};
118 0         0 push @$help_options, [$option_name, $description];
119             }
120              
121 0         0 $string .= $this->_options_format($help_options, 2);
122              
123             # Remove trailing whitespace
124 0         0 while (chomp $string > 0) {}
125              
126 0         0 return $string;
127             }
128              
129             sub _options_format {
130 0     0   0 my ($this, $options, $indent_level) = @_;
131 0 0       0 $indent_level = 1 if ( not defined $indent_level );
132              
133 0         0 my $max_length = 0;
134 0         0 foreach my $pair (@$options) {
135 0         0 my ($name) = @$pair;
136 0         0 my $name_length = length($name);
137 0 0       0 $max_length = $name_length if ( $name_length > $max_length);
138             }
139              
140 0         0 my $string = '';
141              
142 0         0 my $description_indent_level = ($indent_level * 3) + $max_length + 4;
143              
144 0         0 foreach my $pair (@$options) {
145 0         0 my ($name, $description) = @$pair;
146 0         0 my $formatted = $this->format_text($description, $description_indent_level);
147 0         0 my $description_prefix = (' ' x ($indent_level * 3)) . '--' . $name;
148              
149 0         0 $description_prefix .= ' ' x ($description_indent_level - length($description_prefix));
150              
151 0         0 my $prefix_size = length($description_prefix);
152 0         0 $string .= $description_prefix;
153 0         0 $string .= substr $formatted, $prefix_size;
154             }
155              
156 0         0 return $string;
157             }
158              
159             {
160             my $size_initialized = 0;
161             my $size = 80;
162             sub get_terminal_size {
163 0 0   0 0 0 if ( ! $size_initialized ) {
164 0         0 $size_initialized = 1;
165 0 0       0 if (eval { require Term::ReadKey; 1 }) {
  0 0       0  
  0         0  
166 0         0 eval {
167 0         0 $size = (Term::ReadKey::GetTerminalSize())[0];
168             };
169             } elsif ($ENV{COLUMNS}) {
170 0         0 $size = $ENV{COLUMNS};
171             }
172             }
173 0         0 return $size;
174             }
175             }
176              
177             sub format_text {
178 0     0 0 0 my ($this, $text, $left_indent) = @_;
179 0   0     0 $left_indent ||= 0;
180 0         0 return autoformat $text, {
181             left => $left_indent + 1,
182             right => get_terminal_size(),
183             all => 1,
184             };
185             }
186              
187             # this is a hook for subclasses
188       11 0   sub add_help_types {
189             }
190              
191             sub use_help_type {
192 375     375 0 450 my $this = shift;
193 375         443 my $type = shift;
194              
195 375         542 $this->{'HELP_TYPES'}->{$type}->{'USE'} = 1;
196 375         591 $this->{'HELP_TYPES'}->{'all'}->{'USE'} = 1;
197             }
198              
199             sub add_help_type {
200 62     62 0 118 my $this = shift;
201 62         100 my $type = shift;
202 62         86 my $action = shift;
203 62         97 my $description = shift;
204 62         89 my $skip_in_all = shift;
205 62   50     213 my $option_name = shift || 0;
206              
207 62         973 $this->{'HELP_TYPES'}->{$type} = {
208             USE => 1,
209             SKIP_IN_ALL => $skip_in_all,
210             CODE => $action,
211             OPTION_NAME => $option_name,
212             DESCRIPTION => $description,
213             };
214             }
215              
216             sub parse_options {
217 168     168 0 841 my $this = shift;
218 168   50     419 my $args = shift || [];
219 168   100     359 my $options_spec = shift || {};
220 168   100     533 my $configuration_options = shift || [];
221              
222             # Add help options
223 168         255 foreach my $help_type (keys %{$this->{'HELP_TYPES'}}) {
  168         684  
224 1406         1728 my $type_info = $this->{'HELP_TYPES'}->{$help_type};
225 1406 100       2190 next unless ( $type_info->{'USE'} );
226              
227 762   66     1786 my $help_option = $type_info->{'OPTION_NAME'} || 'help-' . $help_type;
228              
229             $options_spec->{$help_option} ||= sub {
230 0     0   0 $type_info->{'CODE'}->($this);
231 0         0 exit 1;
232 762   50     3138 };
233             }
234              
235             # Add filename annotation option
236 168 50       607 $options_spec->{'--filename-key|fk=s'} = \($this->{'FILENAME_KEY'})
237             if $this->does_record_output;
238              
239 168         612 my $starting_config = Getopt::Long::Configure();
240              
241             # Push custom configuration
242 168         2251 Getopt::Long::Configure('no_ignore_case', @$configuration_options);
243              
244 168         5033 local @ARGV = @$args;
245 168 50       774 unless (GetOptions(%$options_spec)) {
246             # output usage if there was a problem with option parsing
247 0         0 $this->_set_wants_help(1);
248             }
249              
250             # Restore original configuration
251 168         50177 Getopt::Long::Configure($starting_config);
252              
253 168         2622 @$args = @ARGV;
254             }
255              
256             sub update_current_filename {
257 4     4 0 12 my ($this, $filename) = @_;
258 4         9 set_current_filename($filename);
259             }
260              
261             sub _set_wants_help {
262 0     0   0 my $this = shift;
263 0         0 my $help = shift;
264              
265 0         0 $this->{'WANTS_HELP'} = $help;
266             }
267              
268             sub get_wants_help {
269 8     8 0 13 my $this = shift;
270 8         25 return $this->{'WANTS_HELP'};
271             }
272              
273             sub _set_exit_value {
274 0     0   0 my $this = shift;
275 0         0 my $value = shift;
276              
277 0         0 $this->{'EXIT_VALUE'} = $value;
278             }
279              
280             sub get_exit_value {
281 0     0 0 0 my $this = shift;
282 0   0     0 return $this->{'EXIT_VALUE'} || 0;
283             }
284              
285             sub print_usage {
286 0     0 0 0 my $this = shift;
287 0         0 my $message = shift;
288              
289 0 0       0 if ( $message ) {
290 0         0 chomp $message;
291 0         0 warn "$message\n";
292              
293 0 0       0 if ( $message =~ m/FATAL/ ) {
294 0         0 return;
295             }
296             }
297              
298 0         0 my $usage = $this->usage();
299              
300             #Remove all trailing newlines
301 0         0 while (chomp $usage > 0) {}
302              
303 0         0 my $formatted_usage = $this->format_usage($usage);
304              
305             #Remove all trailing newlines
306 0         0 while (chomp $formatted_usage > 0) {}
307              
308 0         0 print $formatted_usage . "\n";
309             }
310              
311             sub format_usage {
312 0     0 0 0 my ($this, $usage) = @_;
313              
314 0         0 my $lines = [split("\n", $usage)];
315              
316 0         0 my $output = '';
317 0         0 my $capturing = 0;
318 0         0 my $accumulator = 0;
319 0         0 my $current_indent = 0;
320              
321 0         0 while(@$lines) {
322 0         0 my $line = shift @$lines;
323 0         0 chomp $line;
324 0 0       0 if ( $line =~ m/^\s*__FORMAT_TEXT__\s*$/ ) {
    0          
325 0 0       0 if ( $capturing ) {
326 0         0 $capturing = 0;
327 0         0 $output .= $this->format_text($accumulator, $current_indent);
328             }
329             else {
330 0         0 $capturing = 1;
331              
332 0         0 my $first_line = shift @$lines;
333 0         0 chomp $first_line;
334 0         0 my ($indention) = $first_line =~ m/^(\s*)/;
335 0         0 $first_line =~ s/\s*//;
336 0         0 $current_indent = length($indention);
337 0         0 $accumulator = $first_line;
338             }
339             }
340             elsif ( $capturing ) {
341 0 0       0 if ( $line =~ m/^\s*$/ ) {
342 0         0 $accumulator .= "\n\n";
343             }
344             else {
345 0         0 $line =~ s/^\s*//;
346 0         0 $accumulator .= " $line";
347             }
348             }
349             else {
350 0         0 $output .= $line . "\n";
351             }
352             }
353              
354 0         0 return $output;
355             }
356              
357       1 0   sub init {
358             }
359              
360             # subclasses can override to indicate they'll handle their own extra
361             # args and input in stream_done()
362             sub wants_input {
363 249     249 0 1012 return 1;
364             }
365              
366             sub does_record_output {
367 168     168 0 834 return 1;
368             }
369              
370             sub finish {
371 165     165 0 919 my $this = shift;
372 165         462 $this->stream_done();
373 163         522 $this->{'NEXT'}->finish();
374             }
375              
376             {
377             my $filename;
378             sub get_current_filename {
379 174   100 174 0 2851 return $filename || 'NONE';
380             }
381              
382             sub set_current_filename {
383 389     389 0 618 my $name = shift;
384 389         621 $filename = $name;
385             }
386             }
387              
388             sub subclass_should_implement {
389 0     0 0 0 my $this = shift;
390 0         0 croak "Subclass should implement: " . ref($this);
391             }
392              
393       47 0   sub stream_done {
394             }
395              
396             sub push_record {
397 440     440 0 685 my ($this, $record) = @_;
398              
399 440 100       802 if ( $this->{'FILENAME_KEY'} ) {
400 14         31 ${$record->guess_key_from_spec($this->{'FILENAME_KEY'})} = get_current_filename();
  14         44  
401             }
402              
403 440         1049 return $this->{'NEXT'}->accept_record($record);
404             }
405              
406             sub push_line {
407 466     466 0 719 my ($this, $line) = @_;
408 466         813 $this->{'NEXT'}->accept_line($line);
409             }
410              
411             sub load_operation {
412 20     20 0 31 my $script = shift;
413              
414 20         25 my $operation = $script;
415              
416 20 50       67 die "Script not named recs-*: $script" unless ( $script =~ s/^recs-// );
417              
418 20         44 my @modules = ("App::RecordStream::Operation::$script");
419 20         63 App::RecordStream::Site::bootstrap();
420 20         44 my @sites = sort { $a->{'priority'} <=> $b->{'priority'} } App::RecordStream::Site::list_sites();
  0         0  
421 20         36 for my $site (@sites)
422             {
423 0         0 unshift @modules, $site->{'path'} . "::Operation::$script";
424             }
425              
426 20         52 my $module;
427             my @errors;
428 20         29 for my $try_module (@modules)
429             {
430 20         916 eval "require $try_module";
431 20 50       68 if($@) {
432 0 0       0 if ($@ =~ m/^Please install missing/ ) {
433 0         0 warn $@;
434 0         0 exit 1;
435             }
436 0         0 push @errors, "Could not load $try_module: $@";
437             }
438             else {
439 20         26 $module = $try_module;
440 20         34 last;
441             }
442             }
443 20 50       38 if(!$module) {
444 0         0 die "Could not find operation $script:\n" . join("", @errors);
445             }
446              
447 20         43 return $module;
448             }
449              
450             sub is_recs_operation {
451 14     14 0 21 my $script = shift;
452              
453 14 100       44 if ( $script =~ m/^recs-/ ) {
454 12         20 eval { load_operation($script) };
  12         17  
455 12 50       23 return 0 if ( $@ );
456 12         41 return 1;
457             }
458              
459 2         6 return 0;
460             }
461              
462             sub create_operation {
463 8     8 0 11 my $script = shift;
464 8         9 my $args = shift;
465 8   33     14 my $next = shift || App::RecordStream::Stream::Printer->new();
466              
467 8         13 my $module = load_operation($script);
468              
469 8         9 my $op;
470 8         10 eval {
471 8         33 $op = $module->new($args, $next);
472             };
473              
474 8 50 33     37 if ( $@ || $op->get_wants_help() ) {
475 0 0       0 if ( ! $op ) {
476 0         0 $op = bless {}, $module;
477 0         0 $op->init_help();
478             }
479 0         0 $op->print_usage($@);
480 0         0 exit 1;
481             }
482              
483 8         23 return $op;
484             }
485              
486             sub basic_help {
487 0     0 0   my $this = shift;
488 0           $this->print_usage($@);
489             }
490              
491             sub all_help {
492 0     0 0   my $this = shift;
493              
494 0           foreach my $type (sort keys %{$this->{'HELP_TYPES'}}) {
  0            
495 0           my $info = $this->{'HELP_TYPES'}->{$type};
496 0 0         next if ( $info->{'SKIP_IN_ALL'} );
497 0 0         next if ( !$info->{'USE'} );
498              
499 0           print "Help from: --help-$type:\n";
500              
501 0           $info->{'CODE'}->($this);
502 0           print "\n"
503             }
504             }
505              
506             sub keys_help {
507 0     0 0   my $this = shift;
508 0           $this->keyspecs_help();
509 0           print "\n";
510 0           $this->keygroups_help();
511             }
512              
513             sub snippet_help {
514 0     0 0   my $this = shift;
515 0           print $this->format_usage(App::RecordStream::Executor::usage());
516             }
517              
518             sub keyspecs_help {
519 0     0 0   my $this = shift;
520 0           print $this->format_usage(App::RecordStream::KeySpec::keyspec_help());
521             }
522              
523             sub keygroups_help {
524 0     0 0   my $this = shift;
525 0           print $this->format_usage(App::RecordStream::KeyGroups::usage());
526             }
527              
528             sub domainlanguage_help {
529 0     0 0   my $this = shift;
530 0           print $this->format_usage(App::RecordStream::DomainLanguage::usage());
531             }
532              
533             sub clumping_help {
534 0     0 0   my $this = shift;
535 0           print $this->format_usage(App::RecordStream::Clumper::usage());
536             }
537              
538             # A static method for a single-line operation bootstrap. Operation wrappers
539             # can/should be a symlink to recs-operation itself or just this one line: use
540             # App::RecordStream::Operation; App::RecordStream::Operation::main();
541             sub main {
542 0   0 0 0   my $command = shift || $Script;
543              
544 0           $| = 1;
545              
546 0 0         if ( $command eq 'recs-operation' ) {
547 0           print <
548             WARNING!
549             recs-operation invoked directly!
550              
551             recs-operation is a wrapper for all other recs commands. You do not want to
552             use this script. It uses the App::RecordStream::Operation::* modules to performation
553             operations, like recs-grep. If you are looking for implementation of those
554             scripts, look in those modules. Otherwise, use a different recs script like
555             recs-grep or recs-collate directly.
556              
557             Terminating program.
558             MESSAGE
559 0           exit 1;
560             }
561              
562 0           my @args = @ARGV;
563 0           @ARGV = ();
564              
565 0           my $op = App::RecordStream::Operation::create_operation($command, \@args);
566              
567 0 0         if ( $op->wants_input() ) {
568 0           @ARGV = @args;
569 0           while(my $line = <>) {
570 0           chomp $line;
571 0           App::RecordStream::Operation::set_current_filename($ARGV);
572 0 0         if ( ! $op->accept_line($line) ) {
573 0           last;
574             }
575             }
576             }
577 0           $op->finish();
578              
579 0           exit $op->get_exit_value();
580             }
581              
582             1;