File Coverage

blib/lib/App/CSE.pm
Criterion Covered Total %
statement 56 59 94.9
branch n/a
condition n/a
subroutine 20 20 100.0
pod n/a
total 76 79 96.2


line stmt bran cond sub pod time code
1 0         0 BEGIN{
2             # Avoid Slurp warnings on perl 5.8
3 11     11   1008843 no warnings 'redefine';
  11         24  
  11         783  
4 11     11   8538 require File::Slurp;
5 11     11   56 use warnings;
  11         19  
  11         266  
6             }
7 11     11   177533 use strict;
  11         21  
  11         272  
8 11     11   52 use warnings;
  11         33  
  11         661  
9             package App::CSE;
10             $App::CSE::VERSION = '0.012';
11              
12 11     11   8634 use Moose;
  11         5098551  
  11         87  
13 11     11   80244 use Class::Load;
  11         31  
  11         777  
14 11     11   74 use Cwd;
  11         23  
  11         917  
15 11     11   9800 use App::CSE::Colorizer;
  11         146472  
  11         800  
16 11     11   15475 use DateTime;
  11         4906441  
  11         724  
17 11     11   9743 use File::MimeInfo::Magic;
  11         99317  
  11         1104  
18 11     11   7009 use IO::Interactive;
  11         96095  
  11         96  
19 11     11   10479 use JSON;
  11         147139  
  11         140  
20 11     11   9112 use String::CamelCase;
  11         6302  
  11         762  
21              
22 11     11   7943 use Path::Class::Dir;
  11         334413  
  11         386  
23 11     11   94 use File::stat;
  11         20  
  11         96  
24 11     11   12741 use Getopt::Long qw//;
  11         129030  
  11         474  
25 11     11   13611 use Regexp::Assemble;
  11         198746  
  11         625  
26 11     11   8870 use Text::Glob;
  11         11071  
  11         647  
27 11     11   25440 use XML::LibXML;
  0            
  0            
28              
29             use Log::Log4perl qw/:easy/;
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             my ($self) = @_;
163             $instance = $self;
164             }
165             sub instance{
166             return $instance;
167             }
168             }
169              
170             sub _build_cseignore{
171             my ($self) = @_;
172             my $file = Path::Class::Dir->new()->file('.cseignore');
173             unless( -e $file ){
174             return;
175             }
176             $LOGGER->info("Will ignore patterns from '$file'");
177             return $file;
178             }
179              
180             sub _build_ignore_reassembl{
181             my ($self) = @_;
182             my $re = Regexp::Assemble->new();
183             if( my $cseignore = $self->cseignore() ){
184             my @lines = split(q/\n/ , $cseignore->slurp());
185             foreach my $line ( @lines ){
186             if( $line =~ /^\s*(?:#|$)/ ){
187             next;
188             }
189             $line =~ s/^\s*//; $line =~ s/\s*$//;
190             $re->add( Text::Glob::glob_to_regex_string( $line ) );
191             }
192             }
193             return $re;
194             }
195              
196             sub _build_xml_parser{
197             my ($self) = @_;
198             return XML::LibXML->new();
199             }
200              
201             sub _build_colorizer{
202             my ($self) = @_;
203             return App::CSE::Colorizer->new( { cse => $self } );
204             }
205              
206             sub _build_interactive{
207             my ($self) = @_;
208             return IO::Interactive::is_interactive();
209             }
210              
211             sub _build_index_meta_file{
212             my ($self) = @_;
213             return $self->index_dir()->file('cse_meta.js');
214             }
215              
216             sub _build_index_dirty_file{
217             my ($self) = @_;
218             return $self->index_dir()->file('cse_dirty.js');
219             }
220              
221             sub _build_index_meta{
222             my ($self) = @_;
223             unless( -r $self->index_meta_file() ){
224             return { version => '-unknown-' };
225             }
226             return JSON::decode_json(File::Slurp::read_file($self->index_meta_file().'' , { binmode => ':raw' }));
227             }
228              
229             sub _build_dirty_files{
230             my ($self) = @_;
231             unless( -r $self->index_dirty_file() ){
232             return {};
233             }
234             return JSON::decode_json(File::Slurp::read_file($self->index_dirty_file().'' , { binmode => ':raw' }));
235             }
236              
237             sub _build_index_mtime{
238             my ($self) = @_;
239             my $st = File::stat::stat($self->index_dir());
240             return DateTime->from_epoch( epoch => $st->mtime() );
241             }
242              
243             sub _build_max_size{
244             my ($self) = @_;
245             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             my ($self) = @_;
250              
251             if( my $opt_idx = $self->options->{idx} ){
252             return Path::Class::Dir->new($opt_idx);
253             }
254              
255             return Path::Class::Dir->new('.cse.idx');
256             }
257              
258             sub _build_command_name{
259             my ($self) = @_;
260              
261             unless( $ARGV[0] ){
262             return 'help';
263             }
264              
265             if( $ARGV[0] =~ /^-/ ){
266             # The first argv is an option. Assume search
267             return 'search';
268             }
269              
270             ## Ok the first argv is a normal string.
271             ## Attempt loading a command class.
272             my $command_class = eval{ Class::Load::load_class(__PACKAGE__.'::Command::'.String::CamelCase::camelize($ARGV[0])) };
273             if( $command_class ){
274             # Valid command class. Return it.
275             return shift @ARGV;
276             };
277              
278              
279             ## This first word is not a valid commnad class.
280             ## Assume search.
281             return 'search';
282              
283             }
284              
285             sub _build_command{
286             my ($self) = @_;
287             my $command_class = Class::Load::load_class(__PACKAGE__.'::Command::'.String::CamelCase::camelize($self->command_name()));
288             my $command = $command_class->new({ cse => $self });
289             return $command;
290             }
291              
292             sub _build_options_specs{
293             my ($self) = @_;
294             return $self->command()->options_specs();
295             }
296              
297             sub _build_options{
298             my ($self) = @_;
299              
300             my %options = ();
301              
302             my $p = Getopt::Long::Parser->new;
303             ## Avoid capturing unknown options, like -hello
304             $p->configure( 'pass_through' );
305             # Beware that accessing options_specs will consume the command as the first ARGV
306             $p->getoptions(\%options , 'idx=s', 'dir=s', 'max-size=i', 'verbose+', @{$self->options_specs()} );
307             return \%options;
308             }
309              
310             sub _build_args{
311             my ($self) = @_;
312             $self->options();
313             my @args = @ARGV;
314             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             my ($self) = @_;
336              
337             unless( Log::Log4perl->initialized() ){
338              
339             binmode STDOUT , ':utf8';
340             binmode STDERR , ':utf8';
341              
342             if( $self->options()->{verbose} ){
343             Log::Log4perl::init(\$verbose_log);
344             }else{
345             Log::Log4perl::init(\$standard_log);
346             }
347             }
348              
349             return $self->command()->execute();
350             }
351              
352             sub save_index_meta{
353             my ($self) = @_;
354             File::Slurp::write_file($self->index_meta_file().'' , { binmode => ':raw' }, JSON::encode_json($self->index_meta));
355             return 1;
356             }
357              
358             sub save_dirty_files{
359             my ($self) = @_;
360             File::Slurp::write_file($self->index_dirty_file().'' , { binmode => ':raw' }, JSON::encode_json($self->dirty_files));
361             return 1;
362             }
363              
364             sub version{
365             my ($self) = @_;
366             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             my ($self, $file_name , $opts ) = @_;
374              
375             unless( defined( $opts ) ){
376             $opts = {};
377             }
378              
379             if( $self->ignore_reassembl()->match( $file_name ) ){
380             $LOGGER->trace("File $file_name is ignoreed. Skipping");
381             return $opts->{on_skip} ? &{$opts->{on_skip}}() : undef;
382             }
383              
384             if( $file_name =~ /(?:\/|^)\.[^\/\.]+/ ){
385             $LOGGER->trace("File $file_name is hidden. Skipping");
386             return $opts->{on_hidden} ? &{$opts->{on_hidden}}() : undef;
387             }
388              
389             unless( -r $file_name ){
390             $LOGGER->warn("Cannot read $file_name. Skipping");
391             return $opts->{on_unreadable} ? &{$opts->{on_unreadable}}() : undef;
392             }
393              
394             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             my ($self, $file_name , $opts) = @_;
406             my $mime_type = File::MimeInfo::Magic::mimetype($file_name.'') || 'application/octet-stream';
407              
408             if( $BLACK_LIST->{$mime_type} ){
409             return;
410             }
411             return $mime_type;
412             }
413              
414             }
415             __PACKAGE__->meta->make_immutable();