File Coverage

blib/lib/File/Searcher.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package File::Searcher;
2              
3 1     1   867 use File::Find;
  1         1  
  1         74  
4 1     1   929 use File::Copy;
  1         6130  
  1         67  
5 1     1   1644 use File::Flock;
  0            
  0            
6             use Class::Struct;
7             use Class::Generate qw(class subclass);
8             use Carp;
9             use strict;
10             use vars qw($VERSION $DEBUG $AUTOLOAD);
11              
12             $VERSION = '0.92';
13             $DEBUG=0;
14              
15              
16             struct Stats => {device_code=>'$', inode_number=>'$', mode_flags=>'$', link_cnt=>'$', user_id=>'$', group_id=>'$', device_type=>'$', size_bytes=>'$', time_access_seconds=>'$', time_modified_seconds=>'$', time_status_seconds=>'$', block_system=>'$', block_file=>'$', time_access_string=>'$', time_modified_string=>'$', time_status_string=>'$', mode_string=>'$',};
17             struct Properties => {readable_e=>'$', writable_e=>'$', executable_e=>'$', readable_r=>'$', writable_r=>'$', executable_r=>'$', owned_e=>'$', owned_r=>'$', exist=>'$', exist_non_zero=>'$', zero_size=>'$', file=>'$', directory=>'$', link_=>'$', pipe_=>'$', socket_=>'$', block=>'$', character=>'$', setuid_bit=>'$', setgid_bit=>'$', sticky_bit=>'$', opened_tty=>'$', text=>'$', binary=>'$', stats=>'Stats', path=>'$', dir=>'$', name=>'$',};
18             struct Match => {match=>'$',pre=>'$',post=>'$',last=>'$',start_offset=>'$',end_offset=>'$',contents=>'$',};
19             class Expression=>{
20             search=>{type=>'$', required=>1, default=>'""'},
21             replace=>{type=>'$', required=>1, default=>'""'},
22             options=>{type=>'$', required=>1, default=>'""'},
23             eval_replacement=>{type=>'$', required=>0},
24             case_insensitive=>{type=>'$', required=>0},
25             multiline=>{type=>'$', required=>0},
26             singleline=>{type=>'$', required=>0},
27             repeat=>{type=>'$', required=>0},
28             extend=>{type=>'$', required=>0},
29             do_all=>{type=>'$', default=>'0'},
30             skip_expression=>{type=>'$', default=>'0'},
31             files_matched=>{type=>'@'},
32             files_replaced=>{type=>'@'},
33             replacements=>{type=>'%'},
34             matches=>{type=>'%'},
35             };
36             class Search=>{
37             start_directory=>{type=>'$',default=>"'./'"},
38             backup_extension=>{type=>'$',default=>"'.bak'"},
39             do_backup=>{type=>'$', default=>'0'},
40             recurse_subs=>{type=>'$', default=>'1'},
41             do_replace=>{type=>'$', default=>'0'},
42             log_mode=>{type=>'$', default=>'111'},
43             do_archive=>{type=>'$', default=>'0'},
44             archive=>{type=>'$'},
45             # expression
46             expression=>{type=>'%Expression'},
47             # constructor
48             file_expression=>{type=>'$'},
49             files=>{type=>'@'},
50             # reports
51             files_matched=>{type=>'@'},
52             file_cnt=>{type=>'$', default=>'0'},
53             file_text_cnt=>{type=>'$', default=>'0'},
54             file_binary_cnt=>{type=>'$', default=>'0'},
55             file_unknown_cnt=>{type=>'$', default=>'0'},
56             unknown_cnt=>{type=>'$', default=>'0'},
57             link_cnt=>{type=>'$', default=>'0'},
58             dir_cnt=>{type=>'$', default=>'0'},
59             socket_cnt=>{type=>'$', default=>'0'},
60             pipe_cnt=>{type=>'$', default=>'0'},
61             };
62              
63              
64             sub new{
65              
66             my $class = shift;
67             my $self = {};
68             bless $self, $class;
69              
70             # new(\@files); explicit file list (array ref context)
71             # new('*.html'); file match expression (scalar context)
72             # new(var=>val,var=>val); search variables (list context)
73             # new(\@files, {var=>val,var=>val});
74             # new('*.html', {var=>val,var=>val});
75             my($files);
76             my $file_expression = '';
77             if(ref $_[1] eq 'HASH')
78             {
79             # new(\@files, {var=>val,var=>val});
80             if(ref $_[0] eq 'ARRAY'){($files) = shift;}
81             # new('*.html', {var=>val,var=>val});
82             else{($file_expression)=shift;}
83             my ($options) = shift;
84             foreach (keys %{$options}){push(@_, $_); push(@_, $options->{$_});}
85             }
86             elsif(ref $_[0] eq 'ARRAY'){$files = shift;} # new(\@files);
87             elsif(scalar(@_) == 1){$file_expression = shift;} # new('*.html');
88             $self->{_search} = Search->new(@_); # @_ will be nothing || list of options
89             $file_expression = $file_expression || $self->file_expression;
90             $file_expression =~ s/([^\\])\./$1\\./g;
91             $file_expression =~ s/^\*/.*/;
92             $file_expression =~ s/\.\./\./g;
93             $self->file_expression($file_expression);
94             $self->files(\@{$files}) unless $self->files > 0;
95             $self->start_directory($self->_get_cwd()) if $self->start_directory eq './';
96             $self->{_on_file_match} = sub {return 1;};
97             $self->{_on_expression_match} = sub {return 1;};
98             $self->{_interactive} = 0;
99             $self->{_expressions} = [];
100              
101             return $self;
102             }
103              
104             sub add_expression{
105             my $self = shift;
106             my (@options) = @_;
107             my @args = ();
108             my $expression_name;
109              
110             while(@options)
111             {
112             my $name = shift @options;
113             my $val = shift @options;
114             if($name =~ /name/i){$expression_name = $val;}
115             else{push(@args, $name); push(@args, $val);}
116             }
117             my $expression = Expression->new(@args);
118             push(@{$self->{_expressions}}, $expression_name) unless $self->{_search}->expression($expression_name);
119             $self->{_search}->expression($expression_name, $expression);
120              
121             }
122              
123             sub on_file_match{
124             my $self = shift;
125             my ($sub_ref) = @_;
126             $self->{_on_file_match} = $sub_ref if @_;
127             }
128              
129             sub on_expression_match{
130             my $self = shift;
131             my ($sub_ref) = @_;
132             $self->{_on_expression_match} = $sub_ref if @_;
133             }
134              
135             sub get_expressions{
136             my $self = shift;
137             my @expressions = $self->{_search}->expression_keys;
138             return \@expressions;
139             }
140              
141             sub start{
142             my $self = shift;
143             &find(sub {$self->_find_file(@_)}, $self->start_directory);
144             }
145              
146             sub _find_file{
147             my $self = shift;
148              
149             my $fullFileDir = $File::Find::dir;
150             my $fullFilePath = $File::Find::name;
151             return if $fullFileDir ne $self->start_directory && $self->recurse_subs == 0;
152             my $extension = $self->backup_extension;
153             return if $fullFilePath =~ /$extension$/;
154             my $file_expression = $self->file_expression || '';
155             my @files = $self->files;
156             my $file_pass = 0;
157             return if $file_expression ne '' && $fullFilePath !~ /$file_expression/;
158             foreach my $file (@files)
159             {$file_pass = 1 if $fullFilePath =~ /\b$file\b/;}
160             return if $file_pass != 1 && @files > 0;
161              
162              
163             $self->_add_file($fullFilePath);
164             $self->_get_properties($fullFilePath);
165             $self->_get_stats($fullFilePath) if $self->{_files}->{$fullFilePath}->file;
166             return unless $self->{_on_file_match}->($self->{_files}->{$fullFilePath});
167              
168             if($self->{_files}->{$fullFilePath}->file)
169             {
170             my $fileName = $fullFilePath;
171             $fileName =~ s/$fullFileDir//;
172             $fileName =~ s/^\///;
173             $self->{_files}->{$fullFilePath}->dir($fullFileDir);
174             $self->{_files}->{$fullFilePath}->name($fileName);
175             if($self->{_files}->{$fullFilePath}->text)
176             {
177             $self->{_search}->file_text_cnt($self->{_search}->file_text_cnt+1);
178             $self->_process_file($fullFilePath);
179             }
180             elsif($self->{_files}->{$fullFilePath}->binary)
181             {
182             $self->{_search}->file_binary_cnt($self->{_search}->file_binary_cnt+1);
183             }
184             else
185             {
186             $self->{_search}->file_unknown_cnt($self->{_search}->file_unknown_cnt+1);
187             }
188             $self->{_search}->file_cnt($self->{_search}->file_cnt+1);
189             print "$fullFileDir/$fileName\n" if $DEBUG;
190              
191             }
192             elsif($self->{_files}->{$fullFilePath}->directory)
193             {
194             $self->{_files}->{$fullFilePath}->dir($fullFilePath);
195             $self->{_files}->{$fullFilePath}->name('');
196             $self->{_search}->dir_cnt($self->{_search}->dir_cnt+1);
197             print "$fullFilePath\n" if $DEBUG;
198             }
199             elsif($self->{_files}->{$fullFilePath}->socket_)
200             {$self->{_search}->socket_cnt($self->{_search}->socket_cnt+1);}
201             elsif($self->{_files}->{$fullFilePath}->pipe_)
202             {$self->{_search}->pipe_cnt($self->{_search}->pipe_cnt+1);}
203             elsif($self->{_files}->{$fullFilePath}->link_)
204             {$self->{_search}->link_cnt($self->{_search}->link_cnt+1);}
205             else
206             {$self->{_search}->unknown_cnt($self->{_search}->unknown_cnt+1);}
207              
208             }
209             sub _process_file{
210             my $self = shift;
211             my ($file) = @_;
212             my @expressions = @{$self->{_expressions}};
213             my $contents='';
214             $self->_backup($file) if $self->do_backup;
215             $self->_archive($file) if $self->do_archive;
216             $self->{_search}->add_files_matched($file);
217              
218              
219             my $lock = new File::Flock $file;
220             open(FILE, $file) || die "Cannot read file $file\n";
221             my @contents = ;
222             $contents = join("", @contents);
223             foreach my $expression (@expressions)
224             {
225             next if $self->{_search}->expression($expression)->skip_expression;
226             my ($match_cnt,$replace_cnt,$skip_next,$do_file,$do_all,$new_match_cnt) = (0)x6;
227             $self->{_search}->expression($expression)->add_files_matched($file);
228             $do_all = $self->expression($expression)->do_all;
229             my ($search, $replace, $options_search, $options_replace) = $self->_get_search($expression);
230             $search = "(?" . $options_search . ")" . $search;
231             while($contents =~ m/$search/g)
232             {
233             my $match = Match->new(match=>$&,pre=>$`,post=>$',last=>$+,start_offset=>@-,end_offset=>@+,contents=>$contents);
234             # to support old perl
235             $match->match("$&"); $match->pre("$`"); $match->post("$'"); $match->last("$+"); $match->start_offset("@-"); $match->end_offset("@+"); $match->contents("$contents");
236             my $return_status = 0;
237             $return_status = $self->{_on_expression_match}->($match, $self->{_search}->expression($expression)) if $do_file != 1 && $do_all != 1 && $new_match_cnt < 1;
238             $contents = $return_status and next if $return_status !~ /\d+/ && $return_status ne '';
239             $self->expression($expression)->do_all(1) and $do_all = 1 if $return_status == 100;
240             $do_file = 1 if $return_status == 10;
241             $self->expression($expression)->skip_expression(1) and last if $return_status == -100;
242             last if $return_status == -10;
243             $skip_next = 1 if $return_status == -1;
244              
245             $skip_next = 1 if $match->match eq $replace;
246             $skip_next = 1 if $new_match_cnt > 0;
247             unless($skip_next)
248             {
249             my $body = $match->match;
250             eval ("\$body =~ s/$search/$replace/$options_replace");
251             $replace_cnt++;
252             $match_cnt++;
253             $contents = $match->pre . $body . $match->post;
254             $new_match_cnt = $match_cnt+1;
255             }
256             $new_match_cnt--;
257             $skip_next = 0;
258             }
259             $self->{_search}->expression($expression)->add_files_replaced($file) if $replace_cnt > 0;
260             $self->{_search}->expression($expression)->replacements($file, $replace_cnt);
261             $self->{_search}->expression($expression)->matches($file, $match_cnt);
262             }
263             close(FILE);
264             if($self->{_search}->do_replace)
265             {
266             open(FILE, ">$file") || die "Cannot read file $file\n";
267             print FILE $contents;
268             close(FILE);
269             }
270             }
271              
272             sub _get_search{
273             my $self = shift;
274             my ($expression) = @_;
275             my $options = $self->{_search}->expression($expression)->options || '';
276             my $search = $self->{_search}->expression($expression)->search || '';
277             my $replace = $self->{_search}->expression($expression)->replace || '';
278             my $multiline = 1 if $self->{_search}->expression($expression)->multiline || $options =~ /m/;
279             my $singleline = 1 if $self->{_search}->expression($expression)->singleline || $options =~ /s/;
280             my $case_insensitive = 1 if $self->{_search}->expression($expression)->case_insensitive || $options =~ /i/;
281             my $eval_replacement = 1 if $self->{_search}->expression($expression)->eval_replacement || $options =~ /e/;
282             my $extend = 1 if $self->{_search}->expression($expression)->extend || $options =~ /x/;
283              
284             my $options_search = '';
285             $options_search .= "m" if $multiline;
286             $options_search .= "i" if $case_insensitive;
287             $options_search .= "s" if $singleline;
288             $options_search .= "x" if $extend;
289             my $options_replace = $options_search;
290             $options_replace .= "e" if $eval_replacement;
291              
292             return($search, $replace, $options_search, $options_replace);
293              
294             }
295              
296             sub _archive{
297             my $self = shift;
298             use Archive::Tar;
299             my ($file) = @_;
300              
301             unless($self->{_search}->archive)
302             {
303             my $archive = $self->{_search}->archive(time . ".tgz");
304             my $dir = $self->{_search}->start_directory;
305             Archive::Tar->create_archive("$dir/$archive", 9, "$file");
306             return;
307             }
308             my $tar = Archive::Tar->new();
309             my $archive = $self->{_search}->archive;
310             my $dir = $self->{_search}->start_directory;
311             $archive = "$dir/$archive";
312             $tar->read($archive,1);
313             $tar->add_files($file);
314             $tar->write($archive,9);
315             }
316              
317             sub _backup{
318             my $self = shift;
319             my ($file) = @_;
320             my $extension = $self->backup_extension;
321             return if $file =~ /$extension$/;
322             copy($file, $file . $extension);
323             }
324              
325              
326             sub _add_file{
327             my $self = shift;
328             my ($file) = @_;
329             return if exists $self->{_files}->{$file};
330             $self->{_files}->{$file} = Properties->new(readable_e=>'0',writable_e=>'0',executable_e=>'0',readable_r=>'0',writable_r=>'0',executable_r=>'0',owned_e=>'0',owned_r=>'0',exist=>'0',exist_non_zero=>'0',zero_size=>'0',file=>'0',directory=>'0',link_=>'0',pipe_=>'0',socket_=>'0',block=>'0',character=>'0',setuid_bit=>'0',setgid_bit=>'0',sticky_bit=>'0',opened_tty=>'0',text=>'0',binary=>'0',path=> $file,);
331             }
332              
333              
334             sub _get_properties{
335             my $self = shift;
336             my ($file) = @_;
337             my %p = (readable_e=>'-r',writable_e=>'-w',executable_e=>'-x',readable_r=>'-R',writable_r=>'-W',executable_r=>'-x',owned_e=>'-o',owned_r=>'-O',exist=>'-e',exist_non_zero=>'-s',zero_size=>'-z',file=>'-f',directory=>'-d',link_=>'-l',pipe_=>'-p',socket_=>'-S',block=>'-b',character=>'-c',setuid_bit=>'-u',setgid_bit=>'-g',sticky_bit=>'-k',opened_tty=>'-t',text=>'-T',binary=>'-B',);
338             foreach (keys %p){eval "\$self->{_files}->{\$file}->$_\(1) if $p{$_} \$file;";}
339             }
340              
341             sub _get_stats{
342             my $self = shift;
343             my ($file) = @_;
344             my @p = qw/device_code inode_number mode_flags link_cnt user_id group_id device_type size_bytes time_access_seconds time_modified_seconds time_status_seconds block_system block_file/;
345             my($device_code, $inode_number, $mode_flags, $link_cnt, $user_id, $group_id, $device_type, $size_bytes, $time_access_seconds, $time_modified_seconds, $time_status_seconds, $block_system, $block_file)
346             = stat($file);
347            
348             foreach (@p){eval("\$self->{_files}->{\$file}->stats(new Stats);"); }
349             foreach (@p){eval("\$self->{_files}->{\$file}->stats->$_\(\$$_);"); }
350              
351             # extra stats
352             $self->{_files}->{$file}->stats->time_access_string($self->_seconds_2_string_date($time_access_seconds));
353             $self->{_files}->{$file}->stats->time_modified_string($self->_seconds_2_string_date($time_modified_seconds));
354             $self->{_files}->{$file}->stats->time_status_string($self->_seconds_2_string_date($time_status_seconds));
355             $self->{_files}->{$file}->stats->mode_string($self->_file_mode_string($mode_flags, $file));
356             }
357              
358             sub _get_cwd{
359             my $self = shift;
360             require Cwd;
361             return Cwd::getcwd;
362             }
363              
364             sub _seconds_2_string_date{
365             my $self = shift;
366             my($time)=@_;
367             require Time::localtime;
368             $time=Time::localtime::ctime($time);
369             return $time;
370             }
371              
372              
373             sub _file_mode_string{
374             my $self = shift;
375             my($mode, $filename) =@_;
376             my($modestring);
377              
378             if (-l $filename){$modestring = "1";}
379             elsif (-d $filename){$modestring = "d";}
380             else{$modestring = "-";}
381             my $oo = $mode & 07;
382             my $go = ($mode >> 3) & 07;
383             my $uo = ($mode >> 6) & 07;
384              
385             $modestring .= ("---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx")[$uo];
386             if (-u $filename){chop($modestring);$modestring .= "s"}
387             $modestring .= ("---", "--x", "-w-", "-wx", "r--", "r-x", "rw-","rwx")[$go];
388             if (-g $filename){chop($modestring);$modestring .= "s"}
389             $modestring .= ("---", "--x", "-w-", "-wx", "r--", "r-x", "rw-","rwx")[$oo];
390              
391             return $modestring;
392             }
393              
394             sub AUTOLOAD{
395             my($package, $function) = ($AUTOLOAD =~ /(.*)::(.*)/);
396             my($self, @args)= @_;
397             # print "$AUTOLOAD, $self," . join(', ', @args) ."\n" if $DEBUG;
398             return $self->{_search}->$function(@args);
399             croak "An Undefined subroutine &$AUTOLOAD";
400             }
401              
402             sub DESTROY{}
403              
404             1;
405             __END__