File Coverage

blib/lib/App/CSE.pm
Criterion Covered Total %
statement 112 173 64.7
branch 8 38 21.0
condition 1 6 16.6
subroutine 35 46 76.0
pod 0 8 0.0
total 156 271 57.5


line stmt bran cond sub pod time code
1 0         0 BEGIN{
2             # Avoid Slurp warnings on perl 5.8
3 11     11   1088517 no warnings 'redefine';
  11         103  
  11         512  
4 11     11   5714 require File::Slurp;
5 11     11   61 use warnings;
  11         17  
  11         201  
6             }
7 11     11   272758 use strict;
  11         24  
  11         229  
8 11     11   51 use warnings;
  11         19  
  11         470  
9             package App::CSE;
10             $App::CSE::VERSION = '0.015';
11              
12 11     11   6128 use Moose;
  11         4596356  
  11         70  
13 11     11   73241 use Class::Load;
  11         24  
  11         487  
14 11     11   58 use Cwd;
  11         22  
  11         733  
15 11     11   6179 use App::CSE::Colorizer;
  11         166855  
  11         515  
16 11     11   10672 use DateTime;
  11         4749698  
  11         532  
17 11     11   5958 use File::MimeInfo::Magic;
  11         84963  
  11         637  
18 11     11   5082 use IO::Interactive;
  11         9129  
  11         70  
19 11     11   7263 use JSON;
  11         105862  
  11         82  
20 11     11   5970 use String::CamelCase;
  11         4861  
  11         531  
21              
22 11     11   5270 use Path::Class::Dir;
  11         169395  
  11         284  
23 11     11   76 use File::stat;
  11         20  
  11         44  
24 11     11   7995 use Getopt::Long qw//;
  11         104082  
  11         341  
25 11     11   8342 use Regexp::Assemble;
  11         161446  
  11         405  
26 11     11   5381 use Text::Glob;
  11         7952  
  11         463  
27 11     11   6885 use XML::LibXML;
  11         484907  
  11         76  
28              
29 11     11   8414 use Log::Log4perl qw/:easy/;
  11         344425  
  11         75  
30              
31             =head1 NAME
32              
33             App::CSE - Code search engine. Implements the 'cse' program
34              
35             =head1 INSTALLATION
36              
37             Using system wide cpan:
38              
39             sudo cpan -i App::CSE
40              
41             Using cpanm:
42              
43             cpanm App::CSE
44              
45             =head1 SYNOPSIS
46              
47             cse
48              
49             See L<App::CSE::Command::Help> For a description the available commands.
50              
51             =head1 FEATURES
52              
53             =over
54              
55             =item Hits highlighting
56              
57             =item Prefix* queries
58              
59             =item Complex queries syntax (Lucy)
60              
61             =item Dirty files indicator
62              
63             =item Directory watcher
64              
65             =item Declaration queries (Perl subs and packages)
66              
67             =item Directory filtering
68              
69             =item Paging
70              
71             =item Ignoring files
72              
73             =item Works with Perl 5.8.8 up to 5.20
74              
75             =back
76              
77             =head1 PROGRAMMATIC USAGE
78              
79             In addition of using this via the command line program 'cse', you can use this app
80             in an object oriented way.
81              
82             For instance:
83              
84             my $app = App::CSE->new( { command_name => 'index',
85             options => { 'idx' => '/path/to/the/index' ,
86             'dir' => '/code/directory/to/index'
87             });
88              
89             if( $app->execute() ){
90             .. and error occured ..
91             }else{
92             .. It is a success ..
93             }
94              
95             Retrieving search hits after a search:
96              
97             my $app = App::CSE->new( { command_name => 'search',
98             args => [ 'search_query' ],
99             options => { 'idx' => '/path/to/the/index' ,
100             'dir' => '/code/directory/to/index'
101             });
102             my $hits = $app->command()->hits();
103             # This is a L<Lucy::Search::Hits>
104              
105             See L<App::CSE::Command::Help> for a list of available commands and options.
106              
107             =head1 LOGGING
108              
109             App::CSE uses L<Log::Log4perl>
110              
111             =head1 BUILD STATUS
112              
113             =begin html
114              
115             <a href="https://travis-ci.org/jeteve/App-CSE"><img src="https://travis-ci.org/jeteve/App-CSE.svg?branch=master"></a>
116              
117             =end html
118              
119             =head1 COPYRIGHT
120              
121             See L<App::CSE::Command::Help>
122              
123             =cut
124              
125             my $LOGGER = Log::Log4perl->get_logger();
126              
127             has 'command_name' => ( is => 'ro', isa => 'Str', required => 1 , lazy_build => 1);
128             has 'command' => ( is => 'ro', isa => 'App::CSE::Command', lazy_build => 1);
129             has 'max_size' => ( is => 'ro' , isa => 'Int' , lazy_build => 1);
130              
131             has 'interactive' => ( is => 'ro' , isa => 'Bool' , lazy_build => 1 );
132             has 'colorizer' => ( is => 'ro' , isa => 'App::CSE::Colorizer' , lazy_build => 1);
133              
134             # GetOpt::Long options specs.
135             has 'options_specs' => ( is => 'ro' , isa => 'ArrayRef[Str]', lazy_build => 1);
136              
137             # The options as slurped by getopts long
138             has 'options' => ( is => 'ro' , isa => 'HashRef[Str]', lazy_build => 1);
139              
140             # The arguments after any option
141             has 'args' => ( is => 'ro' , isa => 'ArrayRef[Str]', lazy_build => 1);
142              
143              
144             has 'index_dir' => ( is => 'ro' , isa => 'Path::Class::Dir', lazy_build => 1);
145             has 'index_mtime' => ( is => 'ro' , isa => 'DateTime' , lazy_build => 1);
146             has 'index_dirty_file' => ( is => 'ro' , isa => 'Path::Class::File', lazy_build => 1);
147             has 'dirty_files' => ( is => 'ro', isa => 'HashRef[Str]', lazy_build => 1);
148              
149             has 'index_meta_file' => ( is => 'ro' , isa => 'Path::Class::File' , lazy_build => 1);
150             has 'index_meta' => ( is => 'ro', isa => 'HashRef[Str]', lazy_build => 1);
151              
152             # File utilities
153             has 'xml_parser' => ( is => 'ro' , isa => 'XML::LibXML', lazy_build => 1);
154              
155             # Environment slurping
156             has 'cseignore' => ( is => 'ro', isa => 'Maybe[Path::Class::File]', lazy_build => 1 );
157             has 'ignore_reassembl' => ( is => 'ro', isa => 'Regexp::Assemble', lazy_build => 1);
158              
159             {# Singleton flavour
160             my $instance;
161             sub BUILD{
162 3     3 0 9 my ($self) = @_;
163 3         69 $instance = $self;
164             }
165             sub instance{
166 0     0 0 0 return $instance;
167             }
168             }
169              
170             sub _build_cseignore{
171 0     0   0 my ($self) = @_;
172 0         0 my $file = Path::Class::Dir->new()->file('.cseignore');
173 0 0       0 unless( -e $file ){
174 0         0 return;
175             }
176 0         0 $LOGGER->info("Will ignore patterns from '$file'");
177 0         0 return $file;
178             }
179              
180             sub _build_ignore_reassembl{
181 0     0   0 my ($self) = @_;
182 0         0 my $re = Regexp::Assemble->new();
183 0 0       0 if( my $cseignore = $self->cseignore() ){
184 0         0 my @lines = split(q/\n/ , $cseignore->slurp());
185 0         0 foreach my $line ( @lines ){
186 0 0       0 if( $line =~ /^\s*(?:#|$)/ ){
187 0         0 next;
188             }
189 0         0 $line =~ s/^\s*//; $line =~ s/\s*$//;
  0         0  
190 0         0 $re->add( Text::Glob::glob_to_regex_string( $line ) );
191             }
192             }
193 0         0 return $re;
194             }
195              
196             sub _build_xml_parser{
197 0     0   0 my ($self) = @_;
198 0         0 return XML::LibXML->new();
199             }
200              
201             sub _build_colorizer{
202 1     1   4 my ($self) = @_;
203 1         33 return App::CSE::Colorizer->new( { cse => $self } );
204             }
205              
206             sub _build_interactive{
207 2     2   5 my ($self) = @_;
208 2         13 return IO::Interactive::is_interactive();
209             }
210              
211             sub _build_index_meta_file{
212 0     0   0 my ($self) = @_;
213 0         0 return $self->index_dir()->file('cse_meta.js');
214             }
215              
216             sub _build_index_dirty_file{
217 2     2   3 my ($self) = @_;
218 2         46 return $self->index_dir()->file('cse_dirty.js');
219             }
220              
221             sub _build_index_meta{
222 0     0   0 my ($self) = @_;
223 0 0       0 unless( -r $self->index_meta_file() ){
224 0         0 return { version => '-unknown-' };
225             }
226 0         0 return JSON::decode_json(File::Slurp::read_file($self->index_meta_file().'' , { binmode => ':raw' }));
227             }
228              
229             sub _build_dirty_files{
230 2     2   4 my ($self) = @_;
231 2 100       51 unless( -r $self->index_dirty_file() ){
232 1         84 return {};
233             }
234 1         101 return JSON::decode_json(File::Slurp::read_file($self->index_dirty_file().'' , { binmode => ':raw' }));
235             }
236              
237             sub _build_index_mtime{
238 0     0   0 my ($self) = @_;
239 0         0 my $st = File::stat::stat($self->index_dir());
240 0         0 return DateTime->from_epoch( epoch => $st->mtime() );
241             }
242              
243             sub _build_max_size{
244 0     0   0 my ($self) = @_;
245 0   0     0 return $self->options()->{max_size} || 1048576; # 1 MB default. This is the buffer size of File::Slurp
246             }
247              
248             sub _build_index_dir{
249 2     2   4 my ($self) = @_;
250              
251 2 50       39 if( my $opt_idx = $self->options->{idx} ){
252 2         24 return Path::Class::Dir->new($opt_idx);
253             }
254              
255 0         0 return Path::Class::Dir->new('.cse.idx');
256             }
257              
258             sub _build_command_name{
259 3     3   6 my ($self) = @_;
260              
261 3 50       8 unless( $ARGV[0] ){
262 0         0 return 'help';
263             }
264              
265 3 50       11 if( $ARGV[0] =~ /^-/ ){
266             # The first argv is an option. Assume search
267 0         0 return 'search';
268             }
269              
270             ## Ok the first argv is a normal string.
271             ## Attempt loading a command class.
272 3         6 my $command_class = eval{ Class::Load::load_class(__PACKAGE__.'::Command::'.String::CamelCase::camelize($ARGV[0])) };
  3         15  
273 3 50       779 if( $command_class ){
274             # Valid command class. Return it.
275 3         78 return shift @ARGV;
276             };
277              
278              
279             ## This first word is not a valid commnad class.
280             ## Assume search.
281 0         0 return 'search';
282              
283             }
284              
285             sub _build_command{
286 3     3   7 my ($self) = @_;
287 3         64 my $command_class = Class::Load::load_class(__PACKAGE__.'::Command::'.String::CamelCase::camelize($self->command_name()));
288 3         207 my $command = $command_class->new({ cse => $self });
289 3         71 return $command;
290             }
291              
292             sub _build_options_specs{
293 3     3   7 my ($self) = @_;
294 3         67 return $self->command()->options_specs();
295             }
296              
297             sub _build_options{
298 3     3   7 my ($self) = @_;
299              
300 3         7 my %options = ();
301              
302 3         25 my $p = Getopt::Long::Parser->new;
303             ## Avoid capturing unknown options, like -hello
304 3         69 $p->configure( 'pass_through' );
305             # Beware that accessing options_specs will consume the command as the first ARGV
306 3         180 $p->getoptions(\%options , 'idx=s', 'dir=s', 'max-size=i', 'verbose+', @{$self->options_specs()} );
  3         86  
307 3         1804 return \%options;
308             }
309              
310             sub _build_args{
311 1     1   3 my ($self) = @_;
312 1         21 $self->options();
313 1         5 my @args = @ARGV;
314 1         24 return \@args;
315             }
316              
317             my $standard_log = q|
318             log4perl.rootLogger= INFO, Screen
319             log4perl.appender.Screen = Log::Log4perl::Appender::Screen
320             log4perl.appender.Screen.stderr = 0
321             log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
322             log4perl.appender.Screen.layout.ConversionPattern=%m%n
323             |;
324              
325             my $verbose_log = q|
326             log4perl.rootLogger= TRACE, Screen
327             log4perl.appender.Screen = Log::Log4perl::Appender::Screen
328             log4perl.appender.Screen.stderr = 0
329             log4perl.appender.Screen.layout = Log::Log4perl::Layout::PatternLayout
330             log4perl.appender.Screen.layout.ConversionPattern = %d [%p] %m%n
331             |;
332              
333              
334             sub main{
335 2     2 0 6 my ($self) = @_;
336              
337 2 50       11 unless( Log::Log4perl->initialized() ){
338              
339 2         17 binmode STDOUT , ':utf8';
340 2         6 binmode STDERR , ':utf8';
341              
342 2 50       58 if( $self->options()->{verbose} ){
343 2         13 Log::Log4perl::init(\$verbose_log);
344             }else{
345 0         0 Log::Log4perl::init(\$standard_log);
346             }
347             }
348              
349 2         8983 return $self->command()->execute();
350             }
351              
352             sub save_index_meta{
353 0     0 0 0 my ($self) = @_;
354 0         0 File::Slurp::write_file($self->index_meta_file().'' , { binmode => ':raw' }, JSON::encode_json($self->index_meta));
355 0         0 return 1;
356             }
357              
358             sub save_dirty_files{
359 1     1 0 6 my ($self) = @_;
360 1         21 File::Slurp::write_file($self->index_dirty_file().'' , { binmode => ':raw' }, JSON::encode_json($self->dirty_files));
361 1         218 return 1;
362             }
363              
364             sub version{
365 2     2 0 14 my ($self) = @_;
366 2   50     19 return $App::CSE::VERSION || 'dev';
367             }
368              
369              
370             # Performs very basic checks on a filename, see if its valid
371             # for indexing.
372             sub is_file_valid{
373 0     0 0   my ($self, $file_name , $opts ) = @_;
374              
375 0 0         unless( defined( $opts ) ){
376 0           $opts = {};
377             }
378              
379 0 0         if( $self->ignore_reassembl()->match( $file_name ) ){
380 0           $LOGGER->trace("File $file_name is ignoreed. Skipping");
381 0 0         return $opts->{on_skip} ? &{$opts->{on_skip}}() : undef;
  0            
382             }
383              
384 0 0         if( $file_name =~ /(?:\/|^)\.[^\/\.]+/ ){
385 0           $LOGGER->trace("File $file_name is hidden. Skipping");
386 0 0         return $opts->{on_hidden} ? &{$opts->{on_hidden}}() : undef;
  0            
387             }
388              
389 0 0         unless( -r $file_name ){
390 0           $LOGGER->warn("Cannot read $file_name. Skipping");
391 0 0         return $opts->{on_unreadable} ? &{$opts->{on_unreadable}}() : undef;
  0            
392             }
393              
394 0           return 1;
395             }
396              
397              
398             # Returns this file mimetype if we find its
399             # not a blacklisted one.
400             {
401             my $BLACK_LIST = {
402             'application/x-trash' => 1
403             };
404             sub valid_mime_type{
405 0     0 0   my ($self, $file_name , $opts) = @_;
406 0   0       my $mime_type = File::MimeInfo::Magic::mimetype($file_name.'') || 'application/octet-stream';
407              
408 0 0         if( $BLACK_LIST->{$mime_type} ){
409 0           return;
410             }
411 0           return $mime_type;
412             }
413              
414             }
415             __PACKAGE__->meta->make_immutable();