File Coverage

blib/lib/App/Askell.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             #
2             # This file is part of App-Askell
3             #
4             # This software is copyright (c) 2013 by Loïc TROCHET.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9             package App::Askell;
10             # ABSTRACT: Execute commands defined by the user when files are created, modified or deleted
11              
12 1     1   33254 use Moose;
  0            
  0            
13             with qw(MooseX::Getopt::Strict);
14             use YAML qw(LoadFile);
15             use Path::Tiny;
16             use AnyEvent::Filesys::Notify;
17             use AnyEvent;
18              
19             our $VERSION = '0.132850'; # VERSION
20              
21             has file
22             => (
23             metaclass => 'Getopt'
24             , is => 'ro'
25             , isa => 'Str'
26             , cmd_aliases => 'f'
27             );
28              
29             has 'version'
30             => (
31             metaclass => 'Getopt'
32             , is => 'ro'
33             , isa => 'Bool'
34             , default => 0
35             , cmd_aliases => 'v'
36             );
37              
38             has 'silent'
39             => (
40             metaclass => 'Getopt'
41             , is => 'ro'
42             , isa => 'Bool'
43             , default => 0
44             , cmd_aliases => 's'
45             );
46              
47             sub _load_file
48             {
49             my $self = shift;
50             my $data = {};
51             my $config = LoadFile($self->file);
52            
53             while (my ($dir, $dir_data) = each %$config)
54             {
55             my $path = path($dir);
56              
57             die "'$dir' is not an absolute path.\n"
58             unless $path->is_absolute;
59              
60             die "'$dir' is not a real directory.\n"
61             unless $path->is_dir;
62            
63             $data->{$dir} = {};
64            
65             while (my ($files_mask, $files_data) = each %$dir_data)
66             {
67             my $cmd = $data->{$dir}->{$files_mask} = {c => undef, m => undef, d => undef};
68            
69             if (ref($files_data))
70             {
71             die "The data associated with '$files_mask' must be a hash.\n"
72             if ref($files_data) ne 'HASH';
73              
74             while (my ($events, $cmd_string) = each %$files_data)
75             {
76             die "One of the commands associated with '$files_mask' is not a string.\n"
77             if ref $cmd_string;
78              
79             for (split(',', $events))
80             {
81             die "'$_' is an invalid event ('c'reated, 'm'odified or 'd'eleted).\n"
82             if $_ ne 'c' and $_ ne 'm' and $_ ne 'd';
83              
84             $cmd->{$_} = $cmd_string;
85             }
86             }
87             }
88             else
89             {
90             $cmd->{c} = $files_data;
91             $cmd->{m} = $files_data;
92             $cmd->{d} = $files_data;
93             }
94             }
95             }
96            
97             return $data;
98             }
99              
100             sub _execute
101             {
102             my ($self, $cmd, $file_name, $vars, $event) = @_;
103              
104             $cmd =~ s/\@p/$vars->{p}/g;
105             $cmd =~ s/\@d/$vars->{d}/g;
106             $cmd =~ s/\@f/$vars->{f}/g;
107             $cmd =~ s/\@b/$vars->{b}/g;
108             $cmd =~ s/\@e/$vars->{e}/g;
109              
110             print "---> $file_name($event) ==> $cmd\n"
111             unless $self->silent;
112              
113             system($cmd) == 0
114             or print STDERR "ERROR: system($cmd) failed - $?\n";
115             }
116              
117             sub _callback
118             {
119             my ($self, $data, @events) = @_;
120              
121             for my $event (@events)
122             {
123             my $path = path($event->path);
124              
125             my $dir_name = $path->dirname;
126             my $file_name = $path->basename;
127              
128             if (exists $data->{$dir_name})
129             {
130             while (my ($files_mask, $cmd) = each %{$data->{$dir_name}})
131             {
132             if ($file_name =~ m/^$files_mask$/)
133             {
134             my $vars = {};
135              
136             $vars->{p} = $event->path;
137             $vars->{d} = $dir_name;
138             $vars->{f} = $file_name;
139              
140             if ($file_name =~ m/^(\S+)\.(\S+)$/)
141             {
142             $vars->{b} = $1;
143             $vars->{e} = $2;
144             }
145             else
146             {
147             $vars->{b} = '';
148             $vars->{e} = '';
149             }
150              
151             $self->_execute($cmd->{c}, $file_name, $vars, 'created') if $event->is_created and $cmd->{c};
152             $self->_execute($cmd->{m}, $file_name, $vars, 'modified') if $event->is_modified and $cmd->{m};
153             $self->_execute($cmd->{d}, $file_name, $vars, 'deleted') if $event->is_deleted and $cmd->{d};
154             }
155             }
156             }
157             }
158              
159             return 0;
160             }
161              
162              
163             sub run
164             {
165             my $self = shift;
166            
167             if ($self->version)
168             {
169             print "askell v$VERSION\n";
170             exit 0;
171             }
172              
173             unless (defined $self->file)
174             {
175             exit 0;
176             }
177              
178             my $data = $self->_load_file;
179              
180             my @watchers;
181              
182             push @watchers, AnyEvent::Filesys::Notify->new
183             (
184             dirs => [ keys %$data ]
185             , interval => 1.0
186             , filter => sub { 1 }
187             , cb => sub { $self->_callback($data, @_) }
188             );
189            
190             print "==>> Press 'Ctrl + C' or enter 'exit', 'quit' or 'bye' to stop the application...\n"
191             unless $self->silent;
192              
193             my $exit = AnyEvent->condvar;
194             push @watchers, AnyEvent->signal(signal => "INT", cb => sub { $exit->send; print "\n" });
195             push @watchers, AnyEvent->io
196             (
197             fh => \*STDIN
198             , poll => 'r'
199             , cb => sub
200             {
201             # Read a line
202             chomp(my $input = <STDIN>);
203             # Quit program if 'exit', 'quit' or 'bye'
204             $exit->send if $input eq 'exit' or $input eq 'quit' or $input eq 'bye';
205             }
206             );
207             $exit->recv;
208             }
209              
210             1;
211              
212             __END__
213              
214             =pod
215              
216             =head1 NAME
217              
218             App::Askell - Execute commands defined by the user when files are created, modified or deleted
219              
220             =head1 VERSION
221              
222             version 0.132850
223              
224             =head1 SYNOPSIS
225              
226             foo.yml
227             =======
228             '/project/foo/src/less/':
229             'app.less':
230             'c,m': lessc --compress @p > @d@b.css
231              
232             askell --file foo.yml
233              
234             baz.yml
235             =======
236             '/project/baz/':
237             '\w+.mpg':
238             'c': mv @p /files/mpg/
239             '\w+.mp3':
240             'c': mv @p /files/mp3/
241             '\w+.xml':
242             'd': rm /files/xml/@f
243              
244             askell --file /project/baz.yml --silent
245              
246             =head1 DESCRIPTION
247              
248             This application allows you to execute commands when some files are created, modified or deleted in directories.
249              
250             The configuration is done via a YAML file where you can specify multiple directories, and several types of files
251             per directory in the form of regular expressions.
252              
253             =head2 Events
254              
255             The following events can be used:
256              
257             B<'c'> --> 'c'reated
258              
259             B<'m'> --> 'm'odified
260              
261             B<'d'> --> 'd'eleted
262              
263             They can also be combined:
264              
265             B<'c,m'> --> 'c'reated or 'm'odifed
266              
267             =head2 Variables
268              
269             The following variables can also be used, they will be replaced by their values at runtime:
270              
271             B<@p> --> full path name (directory and filename)
272              
273             B<@d> --> only directory
274              
275             B<@f> --> only filename
276              
277             B<@b> --> file basename if filename like '*.*' else empty string
278              
279             B<@e> --> file extension if filename like '*.*' else empty string
280              
281             See L<askell> for the syntax of the command line.
282              
283             =for Pod::Coverage::TrustPod run
284              
285             =encoding utf8
286              
287             =head1 AUTHOR
288              
289             Loïc TROCHET <losyme@cpan.org>
290              
291             =head1 COPYRIGHT AND LICENSE
292              
293             This software is copyright (c) 2013 by Loïc TROCHET.
294              
295             This is free software; you can redistribute it and/or modify it under
296             the same terms as the Perl 5 programming language system itself.
297              
298             =cut