File Coverage

blib/lib/App/cpangrep.pm
Criterion Covered Total %
statement 34 154 22.0
branch 1 68 1.4
condition 0 31 0.0
subroutine 12 29 41.3
pod 0 13 0.0
total 47 295 15.9


line stmt bran cond sub pod time code
1             package App::cpangrep;
2              
3 1     1   14869 use strict;
  1         2  
  1         34  
4 1     1   4 use warnings;
  1         2  
  1         22  
5 1     1   14 use 5.008_005;
  1         6  
  1         24  
6 1     1   537 use utf8;
  1         8  
  1         4  
7 1     1   490 use open OUT => qw< :encoding(UTF-8) :std >;
  1         903  
  1         5  
8              
9             our $VERSION = '0.07';
10              
11 1     1   10227 use Config;
  1         1  
  1         54  
12 1     1   665 use URI::Escape qw(uri_escape);
  1         1576  
  1         77  
13 1     1   795 use LWP::UserAgent;
  1         35616  
  1         32  
14 1     1   621 use JSON qw(decode_json);
  1         9838  
  1         6  
15 1     1   632 use CPAN::DistnameInfo;
  1         729  
  1         30  
16 1     1   5 use List::Util qw(sum);
  1         2  
  1         2516  
17              
18             our $SERVER = "http://grep.cpan.me";
19             our $COLOR;
20             our $DEBUG;
21              
22             # TODO:
23             #
24             # • Add paging data to api results and support for page=N parameter
25             #
26             # • Support pages, first with --page, then with 'cpangrep
27             # next' and 'cpangrep prev' or similar? something smarter?
28             #
29              
30             sub run {
31 0     0 0   require Getopt::Long;
32 0           Getopt::Long::GetOptions(
33             'color!' => \$COLOR,
34             'd|debug!' => \$DEBUG,
35             'h|help' => \(my $help),
36             'version' => \(my $version),
37             'pager!' => \(my $pager),
38              
39             'l' => \(my $list),
40             'server=s' => \$SERVER,
41             );
42              
43 0 0 0       setup_colors() unless defined $COLOR and not $COLOR;
44 0 0 0       setup_pager() unless defined $pager and not $pager;
45              
46 0 0         if ($help) {
    0          
    0          
47 0           print help();
48 0           return 0;
49             }
50             elsif ($version) {
51 0           print "cpangrep version $VERSION\n";
52 0           return 0;
53             }
54             elsif (not @ARGV) {
55 0           warn "A query is required.\n\n";
56 0           print help();
57 0           return 1;
58             }
59             else {
60 0           my $query = join " ", @ARGV;
61 0           debug("Using query «$query»");
62              
63 0 0         my $search = search($query)
64             or return 2;
65              
66 0 0         if ($list) {
67 0           display_list($search);
68             } else {
69 0           display($search);
70             }
71 0           return 0;
72             }
73 0           return 0;
74             }
75              
76             sub help {
77 0     0 0   return <<' USAGE';
78             usage: cpangrep [--debug]
79              
80             The query is a Perl regular expression without look-ahead/look-behind.
81             Several operators are supported as well for advanced use.
82              
83             See for more information.
84              
85             Multiple query arguments will be joined with spaces for convenience.
86              
87             -l List only matching filenames. Note that omitted results are
88             not mentioned, but your pattern is likely to match many more
89             files than output.
90              
91             --color Enable colored output even if STDOUT isn't a terminal
92             --no-color Disable colored output
93             --no-pager Disable output through a pager
94              
95             --server Specifies an alternate server to use, for example:
96             --server http://localhost:5000
97              
98             --debug Print debug messages to stderr
99             --help Show this help and exit
100             --version Show version
101              
102             USAGE
103             }
104              
105 0     0 0   sub search_url { "$SERVER/?q=" . uri_escape(shift) }
106 0     0 0   sub search_api_url { "$SERVER/api?q=" . uri_escape(shift) }
107              
108             sub search {
109 0     0 0   my $query = shift;
110 0           my $ua = LWP::UserAgent->new(
111             agent => "cpangrep/$VERSION",
112             );
113 0           $ua->env_proxy;
114              
115 0           my $response = $ua->get( search_api_url($query) );
116              
117 0 0         if (not $response->is_success) {
118 0           warn "Request failed: ", $response->status_line, "\n";
119 0           return;
120             }
121              
122 0           my $content = $response->decoded_content;
123 0           debug("Successfully received " . length($content) . " bytes");
124              
125 0           my $result = eval { decode_json($content) };
  0            
126 0 0 0       if ($@ or not $result) {
127 0           warn "Error decoding JSON response: $@\n";
128 0           debug($content);
129 0           return;
130             }
131 0           return $result;
132             }
133              
134             sub display {
135 0 0   0 0   my $search = shift or return;
136 0   0       my $results = $search->{results} || [];
137 0 0         printf "%d result%s in %d file%s.",
    0          
138             $search->{count}, ($search->{count} != 1 ? "s" : ""),
139             scalar @$results, (@$results != 1 ? "s" : "");
140              
141 0           my $display_total = sum map { scalar @{$_->{results}} }
  0            
  0            
142 0           map { @{$_->{files}} }
  0            
143             @$results;
144 0 0 0       printf " Showing first %d results.", $display_total
145             if $display_total and $display_total != $search->{count};
146 0           print "\n\n";
147              
148 0           for my $result (@$results) {
149 0           my $fulldist = $result->{dist};
150 0           $fulldist =~ s{^(?=(([A-Z])[A-Z]))}{$2/$1/};
151 0           my $dist = CPAN::DistnameInfo->new($fulldist);
152              
153 0           for my $file (@{$result->{files}}) {
  0            
154 0           print colored(["GREEN"], join("/", $dist->cpanid, $dist->distvname, $file->{file})), "\n";
155              
156 0           for my $match (@{$file->{results}}) {
  0            
157 0           my $snippet = $match->{text};
158              
159 0           my ($start, $len) = @{$match->{match}};
  0            
160 0           $len -= $start;
161              
162             # XXX TODO: Track down the grep.cpan.me api bug that causes
163             # this. An example that fails for me today:
164             #
165             # cpangrep dist:App-Prefix file:Changes '^\s*\[.+?\]\s*$'
166             #
167             # -trs, 29 Jan 2014
168 0 0         if (length $snippet < $start + $len) {
169 0           warn colored("API returned an out of bounds match; skipping! (use --debug to see details)", "RED"),
170             color("reset"), "\n";
171 0 0         if ($DEBUG) {
172 0           require Data::Dumper;
173 0           debug("snippet: «$snippet» (length ", length $snippet, ")");
174 0           debug("reported match starts at «$start», length «$len» (ends at «@{[$start+$len]}»)");
  0            
175 0           debug("raw match response: ", Data::Dumper::Dumper($match));
176             }
177 0           next;
178             }
179              
180 0           substr($snippet, $start, $len) = colored(substr($snippet, $start, $len), "BOLD RED");
181              
182 0 0         if ($match->{line}) {
183 0           my $ln = $match->{line}[0] - (substr($snippet, 0, $start) =~ y/\n//);
184             my $print_ln = sub {
185 0     0     colored($ln++, "BLUE") . colored(":", "CYAN")
186 0           };
187 0           $snippet =~ s/^/$print_ln->()/mge;
  0            
188             }
189              
190 0           chomp $snippet;
191 0           print $snippet, color("reset"), "\n\n";
192             }
193 0 0         printf colored(" → %d more match%s from this file.\n\n", "MAGENTA"),
    0          
194             $file->{truncated}, ($file->{truncated} != 1 ? "es" : "")
195             if $file->{truncated};
196             }
197 0 0         printf colored("→ %d more file%s matched in %s.\n\n", "MAGENTA"),
    0          
198             $result->{truncated}, ($result->{truncated} != 1 ? "s" : ""), $dist->distvname
199             if $result->{truncated};
200             }
201             }
202              
203             sub display_list {
204 0 0   0 0   my $search = shift or return;
205 0   0       my $results = $search->{results} || [];
206 0           for my $result (@$results) {
207 0           my $fulldist = $result->{dist};
208 0           $fulldist =~ s{^(?=(([A-Z])[A-Z]))}{$2/$1/};
209 0           my $dist = CPAN::DistnameInfo->new($fulldist);
210              
211 0           for my $file (@{$result->{files}}) {
  0            
212 0           print join("/", $dist->cpanid, $dist->distvname, $file->{file}), "\n";
213             }
214             }
215             }
216              
217             # Some tricks borrowed from uninames' fork_output()
218             sub setup_pager {
219 0 0   0 0   return unless -t STDOUT;
220              
221 0   0       my $pager = $ENV{PAGER} || 'less';
222              
223 0 0 0       $ENV{LESS} = 'SRFX' . ($ENV{LESS} || '')
224             if $pager =~ /less/;
225 0 0 0       $ENV{LESSCHARSET} = "utf-8"
      0        
226             if $pager =~ /more|less/ and ($ENV{LESSCHARSET} || "") ne "utf-8";
227              
228 0 0         open STDOUT, "| $pager"
229             or die "couldn't reopen stdout to pager '$pager': $!\n";
230              
231             # exit cleanly on :q in less
232 0     0     $SIG{PIPE} = sub { exit };
  0            
233              
234             # close piped output, otherwise we screw up terminal
235 1 50   1   411 END { close STDOUT or die "error closing stdout: $!\n" }
236              
237 0           binmode STDOUT, ':encoding(UTF-8)';
238 0           $| = 1;
239             }
240              
241             # Setup colored output if we have it
242             sub setup_colors {
243 0     0 0   eval { require Term::ANSIColor };
  0            
244 0 0 0       if ( not $@ and supports_color() ) {
245 0           $Term::ANSIColor::EACHLINE = "\n";
246 0           *color = *_color_real;
247 0           *colored = *_colored_real;
248             }
249             }
250              
251             # No-op passthrough defaults
252 0     0 0   sub color { "" }
253 0 0   0 0   sub colored { ref $_[0] ? @_[1..$#_] : $_[0] }
254 0     0     sub _color_real { Term::ANSIColor::color(@_) }
255 0     0     sub _colored_real { Term::ANSIColor::colored(@_) }
256              
257             sub supports_color {
258             # We're not on a TTY and don't force it, kill color
259 0 0 0 0 0   return 0 unless -t *STDOUT or $COLOR;
260              
261 0 0         if ( $Config{'osname'} eq 'MSWin32' ) {
262 0           eval { require Win32::Console::ANSI; };
  0            
263 0 0         return 1 if not $@;
264             }
265             else {
266 0 0         return 1 if $ENV{'TERM'} =~ /^(xterm|rxvt|linux|ansi|screen)/;
267 0 0         return 1 if $ENV{'COLORTERM'};
268             }
269 0           return 0;
270             }
271              
272             sub debug {
273 0 0   0 0   return unless $DEBUG;
274 0           warn "DEBUG: ", @_, " [", join("/", (caller(1))[3,2]), "]\n";
275             }
276              
277             1;
278             __END__