File Coverage

blib/lib/App/Prolix.pm
Criterion Covered Total %
statement 100 255 39.2
branch 13 60 21.6
condition 0 6 0.0
subroutine 24 45 53.3
pod 0 23 0.0
total 137 389 35.2


line stmt bran cond sub pod time code
1 1     1   25827 use strict;
  1         2  
  1         39  
2 1     1   6 use warnings;
  1         2  
  1         35  
3 1     1   1320 use Getopt::Long qw(:config no_auto_version);
  1         16065  
  1         7  
4              
5             package App::Prolix;
6             # ABSTRACT: trim chatty command outputs
7              
8 1     1   10787 use Moose;
  1         750600  
  1         12  
9 1     1   11640 use String::ShellQuote ();
  1         1199  
  1         29  
10              
11 1     1   20 use v5.10;
  1         4  
  1         67  
12              
13             {
14             package App::Prolix::ConfigFileRole;
15              
16 1     1   1034 use Moose::Role;
  1         7149  
  1         8  
17             with "MooseX::ConfigFromFile";
18 1     1   10261 use JSON 2.0;
  1         22446  
  1         10  
19              
20             sub get_config_from_file {
21 0     0 0 0 my($file) = @_;
22 0 0       0 open my $fh, "<", $file or confess "open: $file: $!";
23 0         0 local $/;
24 0         0 my $json = <$fh>;
25 0 0       0 close $fh or die "close: $file: $!";
26 0         0 return JSON->new->relaxed->utf8->decode($json);
27             }
28              
29             }
30              
31 1     1   1445 use Data::Munge;
  1         1480  
  1         98  
32 1     1   1382 use IO::File;
  1         21862  
  1         146  
33 1     1   1795 use IPC::Run ();
  1         56470  
  1         27  
34 1     1   961 use Term::ReadKey ();
  1         13781  
  1         27  
35 1     1   1214 use Term::ReadLine;
  1         4048  
  1         19  
36 1     1   1318 use Text::Balanced ();
  1         13008  
  1         26  
37 1     1   13 use Try::Tiny;
  1         2  
  1         59  
38              
39 1     1   10143 use App::Prolix::MooseHelpers;
  1         3  
  1         8  
40              
41             with "MooseX::Getopt";
42              
43             # Flags affecting overall run style.
44             has_option "verbose" => (isa => "Bool", cmd_aliases => "v",
45             documentation => "Prints extra information.");
46             has_option "pipe" => (isa => "Bool", cmd_aliases => "p",
47             documentation => "Reads from stdin instead of interactively.");
48             has_option "log" => (isa => "Str", cmd_aliases => "l",
49             documentation => q{Logs output to a filename (say "auto" } .
50             q{to let prolix pick one for you)});
51              
52             # Flags affecting filtering.
53             has_option "ignore_re" => (isa => "ArrayRef", cmd_aliases => "r",
54             "default" => sub { [] },
55             documentation => "Ignore lines matching this regexp.");
56             has_option "ignore_line" => (isa => "ArrayRef", cmd_aliases => "n",
57             "default" => sub { [] },
58             documentation => "Ignore lines exactly matching this.");
59             has_option "ignore_substring" => (isa => "ArrayRef", cmd_aliases => "b",
60             "default" => sub { [] },
61             documentation => "Ignore lines containing this substring.");
62             has_option "snippet" => (isa => "ArrayRef", cmd_aliases => "s",
63             "default" => sub { [] },
64             documentation => "Snip lines. Use s/search_re/replace/ syntax.");
65              
66             # Internal attributes (leading _ means not GetOpt).
67             has_rw "_cmd" => (isa => "ArrayRef", "default" => sub { [] });
68              
69             has_rw "_out" => (isa => "ScalarRef[Str]", default => \&_strref);
70             has_rw "_err" => (isa => "ScalarRef[Str]", default => \&_strref);
71              
72             has_rw "_log" => (isa => "FileHandle");
73             has_rw "_term" => (
74             isa => "Ref");
75             # TODO(gaal): figure out how to fix this:
76             # isa => "Term::ReadLine|Term::ReadLine::Perl|Term::ReadLine::Gnu");
77             has_rw "_snippet" => (isa => "ArrayRef", "default" => sub { [] });
78             has_rw "_ignore_re" => (isa => "ArrayRef", "default" => sub { [] });
79              
80             has_counter "_suppressed";
81             has_counter "_output_lines";
82              
83             sub run {
84 0     0 0 0 my($self) = @_;
85            
86 0 0       0 if ($self->verbose) {
87 0         0 $SIG{USR1} = \&_dump_stack;
88             }
89              
90 0         0 $self->open_log;
91 0         0 $self->import_re($_) for @{$self->ignore_re};
  0         0  
92 0         0 $self->import_snippet($_) for @{$self->snippet};
  0         0  
93              
94 0 0       0 if ($self->need_pipe) {
95 0         0 $self->run_pipe;
96             } else {
97 0         0 $self->run_spawn;
98             }
99              
100 0 0       0 if ($self->verbose) {
101 0         0 say "Done. " . $self->stats;
102             }
103              
104 0         0 $self->close_log;
105             }
106              
107             sub need_pipe {
108 0     0 0 0 my($self) = @_;
109 0   0     0 return $self->pipe || @{$self->_cmd} == 0;
110             }
111              
112             sub open_log {
113 0     0 0 0 my($self) = @_;
114              
115 0 0       0 return if not defined $self->log;
116              
117 0         0 my $now = $self->now_stamp;
118 0         0 my $filename = $self->log;
119 0 0       0 $filename = ($self->need_pipe ? "prolix.%d" : ($self->_cmd->[0] . ".%d")) if
    0          
120             $filename eq "auto";
121 0 0       0 $filename = File::Spec->catfile(File::Spec->tmpdir, $filename) if
122             $filename !~ m{[/\\]}; # Put in /tmp/ or similar unless we got a path.
123 0         0 $filename =~ s/%d/$now/; # TODO(gaal): implement incrementing %n.
124              
125 0 0       0 say "Logging output to $filename" if $self->verbose;
126              
127 0 0       0 my $fh = IO::File->new($filename, "w") or die "open: $filename: $!";
128 0         0 $self->_log($fh);
129             }
130              
131             sub close_log {
132 0     0 0 0 my($self) = @_;
133 0 0       0 $self->_log->close if $self->_log;
134             }
135              
136             # Like: (DateTime->new->iso8601 =~ s/[-:]//g), but I didn't want to add
137             # a big dependency.
138             sub now_stamp {
139 0     0 0 0 my($self) = @_;
140              
141 0         0 my(@t) = localtime; # Should this be gmtime?
142 0         0 return sprintf "%4d%02d%02dT%02d%02d%02d",
143             $t[5] + 1900, $t[4] + 1, $t[3], $t[2], $t[1], $t[0]; # Ahh, UNIX.
144             }
145              
146             sub stats {
147 0     0 0 0 my($self) = @_;
148 0         0 return "Suppressed " . $self->_suppressed . "/" .
149             $self->_output_lines . " lines.";
150             }
151              
152             # returns a fresh reference to a string.
153             sub _strref {
154 10     10   7411 return \(my $throwaway = "");
155             }
156              
157             sub run_pipe {
158 0     0 0 0 my($self) = @_;
159              
160 0 0       0 say "Running in pipe mode" if $self->verbose;
161              
162 0         0 while (<STDIN>) {
163 0         0 chomp;
164 0         0 $self->on_out($_)
165             }
166             }
167              
168             sub run_spawn {
169 0     0 0 0 my($self) = @_;
170 0         0 say "Running: " .
171 0 0       0 String::ShellQuote::shell_quote_best_effort(@{$self->_cmd})
172             if $self->verbose;
173              
174 0         0 Term::ReadKey::ReadMode("noecho");
175 1     1   1130 END { Term::ReadKey::ReadMode("normal"); }
176              
177 0         0 $self->_term(Term::ReadLine->new("prolix"));
178 0         0 my $attribs = $self->_term->Attribs;
179 0         0 $attribs->{completion_entry_function} =
180             $attribs->{list_completion_function};
181 0         0 $attribs->{completion_word} = [qw(
182             help
183             ignore_line
184             ignore_re
185             ignore_substring
186             pats
187             quit
188             snippet
189             stats
190             )];
191              
192 0         0 my $t = IPC::Run::timer(0.3);
193 0         0 my $ipc = IPC::Run::start $self->_cmd,
194             \undef, # no stdin
195             $self->_out,
196             $self->_err,
197             $t;
198 0         0 $t->start;
199 0         0 my $pumping = 1;
200 0   0     0 while ($pumping && $ipc->pump) {
201 0         0 $self->consume;
202             try {
203 0     0   0 $self->try_user_input;
204             } catch {
205 0     0   0 when (/prolix-quit/) {
206 0         0 $ipc->kill_kill;
207 0         0 $pumping = 0;
208             }
209 0         0 default { die $_ }
  0         0  
210 0         0 };
211 0         0 $t->start(0.3);
212             }
213 0         0 $t->reset;
214 0         0 $ipc->finish;
215 0         0 $self->consume_final;
216              
217 0         0 Term::ReadKey::ReadMode("normal");
218             }
219              
220             sub _dump_stack {
221 0     0   0 print Carp::longmess("************");
222 0         0 $SIG{USR1} = \&_dump_stack;
223             }
224              
225             sub try_user_input {
226 0     0 0 0 my($self) = @_;
227 0 0       0 return if not defined Term::ReadKey::ReadKey(-1);
228              
229             # Enter interactive prompt mode. We hope this will be brief, and
230             # IPC::Run can buffer our watched command in the meanhwile.
231              
232 0 0       0 say q{Press ENTER to go back, or enter "help" for a list of commands.}
233             if $self->verbose;
234              
235 0         0 Term::ReadKey::ReadMode("normal");
236 0         0 while (my $cmd = $self->_term->readline("prolix>")) {
237 0         0 $self->_term->addhistory($cmd);
238 0         0 $self->handle_user_input($cmd);
239             }
240 0         0 Term::ReadKey::ReadMode("restore"); # into noecho, we hope!
241             }
242              
243             sub handle_user_input {
244 0     0 0 0 my($self, $cmd) = @_;
245 0         0 (my $nullary = $cmd) =~ s/^\s*(\S+)\s*/$1/;
246 0 0       0 if ($nullary) {
247 0         0 given ($nullary) {
248 0         0 when ("clear_all") { $self->clear_all }
  0         0  
249 0         0 when ("stack") { _dump_stack }
  0         0  
250 0         0 when ("bufs") { $self->dump_bufs }
  0         0  
251 0         0 when (/q|quit/) { die "prolix-quit\n" }
  0         0  
252 0         0 when (/h|help/) { $self->help_interactive }
  0         0  
253 0         0 when ("pats") { $self->dump_pats }
  0         0  
254 0         0 when ("stats") { say $self->stats }
  0         0  
255 0         0 default { say q{Unknown command. Try "help".} }
  0         0  
256             }
257             } else {
258 0         0 given ($cmd) {
259 0         0 when (/^\s*(ignore_(?:line|re|substring))\s+(.*)/) {
260 0         0 my($ignore_type, $pat) = ($1, $2);
261 0         0 push @{ $self->$ignore_type }, $pat;
  0         0  
262 0 0       0 $self->import_re($pat) if $ignore_type eq 're';
263             }
264 0         0 when (/^\s*snippet\s(.*)/) {
265 0         0 push @{ $self->snippet }, $1;
  0         0  
266 0         0 $self->import_snippet($1);
267             }
268 0         0 default { say q{Unknown command. Try "help".} }
  0         0  
269             }
270             }
271             }
272              
273             sub import_re {
274 2     2 0 2638 my($self, $pat) = @_;
275              
276 2         5 push @{ $self->_ignore_re }, qr/$pat/;
  2         10  
277             }
278              
279             sub import_snippet {
280 7     7 0 1380 my($self, $snippet) = @_;
281              
282 7         14 my $help = <<".";
283             *** Usage: snippet s/find_re/replace/
284              
285             You may use Perl-like quoting on the substitution operation, so if your
286             pattern contains slashes use a different delimiter.
287              
288             Modifiers that are honored: /igx (m and s aren't meaningful here)
289             .
290              
291 7         28 my @extract = Text::Balanced::extract_quotelike($snippet);
292 7         1181 my($op, $search, $replace, $modifiers) = @extract[3, 5, 8, 10];
293 7 50       23 die $help unless $op eq "s";
294 7 50       17 die $help unless defined $search;
295 7 50       16 die $help unless defined $replace;
296 7         11 my $mods = "";
297 7         14 for (qw/i x/) {
298 14 100       164 $mods .= $_ if $modifiers =~ /$_/;
299             }
300 7 100       22 my $global = $modifiers =~ /g/ ? "g" : "";
301 7         112 my $search_re = qr/(?$mods:$search)/;
302              
303 7         32 push @{ $self->_snippet }, sub {
304 10     10   194 my($line) = @_;
305 10         37 return Data::Munge::replace($line, $search_re, $replace, $global);
306 7         12 };
307             }
308              
309             sub dump_pats {
310 0     0 0 0 my($self) = @_;
311              
312 0         0 say "* ignored lines";
313 0         0 say for @{ $self->ignore_line };
  0         0  
314 0         0 say "* ignored patterns";
315 0         0 say for @{ $self->ignore_re };
  0         0  
316 0         0 say "* ignored substrings";
317 0         0 say for @{ $self->ignore_substring };
  0         0  
318 0         0 say "* snippets";
319 0         0 say for @{ $self->snippet };
  0         0  
320             }
321              
322             sub help_interactive {
323 0     0 0 0 my($self) = @_;
324              
325 0         0 say <<"EOF";
326             clear_all - clear all patterns
327             ignore_line - add a full match to ignore
328             ignore_re - add an ignore pattern, e.g. ^(FINE|DEBUG)
329             ignore_substring - add a partial match to ignore
330             pats - list ignore patterns
331             quit - terminate running program
332             stats - print stats
333             snippet - add a snippet expression, e.g. s/^(INFO|WARNING|ERROR) //
334              
335             To keep going, just enter an empty line.
336             EOF
337             }
338              
339             sub clear_all {
340 2     2 0 6 my($self) = @_;
341              
342 2         4 @{ $self->ignore_line } = ();
  2         9  
343 2         14 @{ $self->ignore_re } = ();
  2         10  
344 2         12 @{ $self->_ignore_re } = ();
  2         10  
345 2         12 @{ $self->ignore_substring } = ();
  2         16  
346 2         12 @{ $self->snippet } = ();
  2         9  
347 2         13 @{ $self->_snippet } = ();
  2         6  
348             }
349              
350             sub dump_bufs {
351 0     0 0 0 my($self) = @_;
352 0         0 warn "Out: [" . ${$self->_out} . "]\n" .
  0         0  
353 0         0 "Err: [" . ${$self->_err} . "]\n";
354             }
355              
356             sub consume {
357 0     0 0 0 my($self) = @_;
358              
359 0         0 while (${$self->_out} =~ s/^(.*?)\n//) {
  0         0  
360 0         0 $self->on_out($1);
361             }
362 0         0 while (${$self->_err} =~ s/^(.*?)\n//) {
  0         0  
363 0         0 $self->on_err($1);
364             }
365             }
366              
367             # like consume, but does not require a trailing newline.
368             sub consume_final {
369 0     0 0 0 my($self) = @_;
370              
371 0 0       0 if (length ${$self->_out} > 0) {
  0         0  
372 0         0 $self->on_out($_) for split /\n/, ${$self->_out};
  0         0  
373             }
374 0 0       0 if (length ${$self->_err} > 0) {
  0         0  
375 0         0 $self->on_err($_) for split /\n/, ${$self->_err};
  0         0  
376             }
377             }
378              
379             sub snip_line {
380 10     10 0 42 my($self, $line) = @_;
381              
382 10         15 $line = $_->($line) for @{$self->_snippet};
  10         36  
383              
384 10         338 return $line;
385             }
386              
387             sub process_line {
388 9     9 0 3162 my($self, $line) = @_;
389              
390 9         14 for my $exact (@{$self->ignore_line}) {
  9         30  
391 2 100       20 if ($line eq $exact) {
392 1         9 return;
393             }
394             }
395 8         50 for my $sub (@{$self->ignore_substring}) {
  8         26  
396 5 100       45 if (index($line, $sub) >= 0) {
397 2         12 return;
398             }
399             }
400 6         27 for my $pat (@{$self->_ignore_re}) {
  6         23  
401 4 100       46 if ($line =~ $pat) {
402 2         13 return;
403             }
404             }
405 4         25 return $self->snip_line($line);
406             }
407              
408             # One day, we might paint this in a different color or something.
409 0     0 0   sub on_err { goto &on_out }
410              
411             sub on_out {
412 0     0 0   my($self, $line) = @_;
413            
414 0           $self->inc__output_lines;
415 0 0         if (defined($line = $self->process_line($line))) {
416 0           say $line;
417 0 0         if ($self->_log) {
418 0           $self->_log->print("$line\n");
419             }
420             } else {
421 0           $self->inc__suppressed;
422             }
423             }
424              
425             6;
426              
427             __END__
428             =pod
429              
430             =head1 NAME
431              
432             App::Prolix - trim chatty command outputs
433              
434             =head1 VERSION
435              
436             version 0.03
437              
438             =head1 AUTHOR
439              
440             Gaal Yahas <gaal@forum2.org>
441              
442             =head1 COPYRIGHT AND LICENSE
443              
444             This software is Copyright (c) 2012 by Google, Inc.
445              
446             This is free software, licensed under:
447              
448             The MIT (X11) License
449              
450             =cut
451