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   1309853 no warnings 'redefine';
  11         120  
  11         554  
4 11     11   7083 require File::Slurp;
5 11     11   63 use warnings;
  11         33  
  11         293  
6             }
7 11     11   314573 use strict;
  11         29  
  11         250  
8 11     11   60 use warnings;
  11         20  
  11         524  
9             package App::CSE;
10             $App::CSE::VERSION = '0.016';
11              
12 11     11   7561 use Moose;
  11         5385095  
  11         76  
13 11     11   83503 use Class::Load;
  11         27  
  11         575  
14 11     11   74 use Cwd;
  11         63  
  11         755  
15 11     11   7099 use App::CSE::Colorizer;
  11         189645  
  11         528  
16 11     11   12977 use DateTime;
  11         5494960  
  11         655  
17 11     11   7818 use File::MimeInfo::Magic;
  11         99169  
  11         756  
18 11     11   6064 use IO::Interactive;
  11         11401  
  11         88  
19 11     11   8542 use JSON;
  11         122149  
  11         93  
20 11     11   6741 use String::CamelCase;
  11         5183  
  11         592  
21              
22 11     11   6623 use Path::Class::Dir;
  11         197493  
  11         324  
23 11     11   88 use File::stat;
  11         20  
  11         60  
24 11     11   9581 use Getopt::Long qw//;
  11         118895  
  11         412  
25 11     11   10898 use Regexp::Assemble;
  11         190232  
  11         478  
26 11     11   6774 use Text::Glob;
  11         9840  
  11         663  
27 11     11   8004 use XML::LibXML;
  11         565098  
  11         83  
28              
29 11     11   9504 use Log::Log4perl qw/:easy/;
  11         405105  
  11         88  
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 11 my ($self) = @_;
163 3         78 $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   6 my ($self) = @_;
203 1         41 return App::CSE::Colorizer->new( { cse => $self } );
204             }
205              
206             sub _build_interactive{
207 2     2   5 my ($self) = @_;
208 2         14 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   5 my ($self) = @_;
218 2         58 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       50 unless( -r $self->index_dirty_file() ){
232 1         106 return {};
233             }
234 1         84 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   5 my ($self) = @_;
250              
251 2 50       60 if( my $opt_idx = $self->options->{idx} ){
252 2         21 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   9 my ($self) = @_;
260              
261 3 50       13 unless( $ARGV[0] ){
262 0         0 return 'help';
263             }
264              
265 3 50       13 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         8 my $command_class = eval{ Class::Load::load_class(__PACKAGE__.'::Command::'.String::CamelCase::camelize($ARGV[0])) };
  3         24  
273 3 50       870 if( $command_class ){
274             # Valid command class. Return it.
275 3         96 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   9 my ($self) = @_;
287 3         77 my $command_class = Class::Load::load_class(__PACKAGE__.'::Command::'.String::CamelCase::camelize($self->command_name()));
288 3         237 my $command = $command_class->new({ cse => $self });
289 3         82 return $command;
290             }
291              
292             sub _build_options_specs{
293 3     3   10 my ($self) = @_;
294 3         84 return $self->command()->options_specs();
295             }
296              
297             sub _build_options{
298 3     3   9 my ($self) = @_;
299              
300 3         7 my %options = ();
301              
302 3         32 my $p = Getopt::Long::Parser->new;
303             ## Avoid capturing unknown options, like -hello
304 3         84 $p->configure( 'pass_through' );
305             # Beware that accessing options_specs will consume the command as the first ARGV
306 3         203 $p->getoptions(\%options , 'idx=s', 'dir=s', 'max-size=i', 'verbose+', @{$self->options_specs()} );
  3         86  
307 3         2218 return \%options;
308             }
309              
310             sub _build_args{
311 1     1   4 my ($self) = @_;
312 1         26 $self->options();
313 1         4 my @args = @ARGV;
314 1         30 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 7 my ($self) = @_;
336              
337 2 50       14 unless( Log::Log4perl->initialized() ){
338              
339 2         23 binmode STDOUT , ':utf8';
340 2         10 binmode STDERR , ':utf8';
341              
342 2 50       76 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         10622 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 7 my ($self) = @_;
360 1         27 File::Slurp::write_file($self->index_dirty_file().'' , { binmode => ':raw' }, JSON::encode_json($self->dirty_files));
361 1         292 return 1;
362             }
363              
364             sub version{
365 2     2 0 15 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();