File Coverage

blib/lib/App/cpangrep.pm
Criterion Covered Total %
statement 33 142 23.2
branch 0 56 0.0
condition 0 19 0.0
subroutine 11 26 42.3
pod 0 12 0.0
total 44 255 17.2


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