File Coverage

blib/lib/Proch/Cmd.pm
Criterion Covered Total %
statement 90 138 65.2
branch 12 38 31.5
condition 11 27 40.7
subroutine 16 23 69.5
pod 3 3 100.0
total 132 229 57.6


line stmt bran cond sub pod time code
1             package Proch::Cmd;
2 2     2   1055 use 5.012;
  2         10  
3 2     2   8 use warnings;
  2         3  
  2         71  
4             $Proch::Cmd::VERSION = 0.007;
5              
6             # ABSTRACT: Execute shell commands with caching capability to store output of executed programs (useful for multi step pipelines where some steps can take long) {beta}
7              
8              
9 2     2   32 use 5.014;
  2         4  
10 2     2   969 use Moose;
  2         783174  
  2         15  
11 2     2   13904 use Data::Dumper;
  2         11585  
  2         135  
12 2     2   15 use Digest::MD5;
  2         4  
  2         62  
13 2     2   1062 use Storable qw(lock_store lock_nstore lock_retrieve);
  2         5420  
  2         126  
14 2     2   14 use Carp qw(confess);
  2         3  
  2         86  
15 2     2   972 use File::Slurp;
  2         51878  
  2         129  
16 2     2   1081 use Time::HiRes qw(gettimeofday tv_interval);
  2         2520  
  2         11  
17 2     2   1240 use Time::Piece;
  2         15980  
  2         10  
18              
19             our %GLOBAL = (
20             'working_dir' => '/tmp',
21             'description' => '<Bash command>',
22             );
23              
24             has debug => ( is => 'rw', isa => 'Bool');
25             has verbose => ( is => 'rw', isa => 'Bool');
26             has die_on_error => ( is => 'rw', isa => 'Bool', default => 1);
27             has no_cache => ( is => 'rw', isa => 'Bool');
28             has save_stderr => ( is => 'rw' );
29              
30             has command => (
31             is => 'rw',
32             required => 1,
33             isa => 'Str',
34             );
35              
36             has description => (
37             is => 'ro',
38             required => 1,
39             isa => 'Str',
40             default => $GLOBAL{description},
41             );
42             has logfile => (
43             is => 'ro',
44             isa => 'Str',
45             );
46              
47             has input_files => (
48             is => 'ro',
49             isa => 'ArrayRef',
50             );
51              
52             has output_files => (
53             is => 'ro',
54             isa => 'ArrayRef',
55             );
56              
57             has cache_file => (
58             is => 'ro',
59             isa => 'Str',
60             );
61              
62              
63              
64             has working_dir => (
65             is => 'rw',
66             isa => 'Str',
67             default => '/tmp/',
68             #builder => '_readdefault_working_dir',
69             );
70              
71             sub _readdefault_working_dir {
72 0     0   0 my ($self) = @_;
73 0         0 say Dumper $self;
74 0         0 return $GLOBAL{"working_dir"};
75             }
76              
77             sub _check_input_files_exist {
78             # All {input_files} need to be found
79 2     2   4 my ($self) = @_;
80 2         3 my @msg = ();
81 2         4 my $errors = 0;
82 2         2 my $output;
83              
84             # Check input input_files
85 2         3 foreach my $file (@{ $self->{input_files} }) {
  2         10  
86 0 0       0 if (! -s "$file") {
87 0         0 $errors++;
88 0         0 push(@msg, qq("$file") );
89             }
90             }
91              
92 2         5 $output->{errors} = $errors;
93 2         5 $output->{raw_messages} = \@msg;
94 2         14 $output->{message} = "Required INPUT file not found: [" . join(', ', @msg) ."] when executing <" .$self->{command} .'>';
95              
96 2 50 66     15 if ($self->{die_on_error} and $output->{errors}){
97 0         0 confess($output->{message});
98             } else {
99 2         5 return $output;
100             }
101             }
102              
103             sub _check_output_files_exist {
104             # All {input_files} need to be found
105 2     2   9 my ($self) = @_;
106 2         10 my @msg = ();
107 2         4 my $errors = 0;
108 2         8 my $output;
109              
110             # Check input input_files
111 2         9 foreach my $file (@{ $self->{output_files} }) {
  2         35  
112 0 0       0 if (! -s "$file") {
113 0         0 $errors++;
114 0         0 push(@msg, qq("$file") );
115             }
116             }
117              
118 2         23 $output->{errors} = $errors;
119 2         11 $output->{raw_messages} = \@msg;
120 2         36 $output->{message} = "Required OUTPUT file not found: [" . join(', ', @msg) ."] when executing <" .$self->{command} .'>';
121              
122 2 50 66     27 if ($self->{die_on_error} and $output->{errors}){
123 0         0 confess($output->{message});
124             } else {
125 2         13 return $output;
126             }
127             }
128             sub simplerun {
129 2     2 1 6746 my ($self) = @_;
130 2         11 my $start_date = localtime->strftime('%m/%d/%Y %H:%M');
131 2         281 my $start_time = [gettimeofday];
132 2         3 my $output;
133 2         6 $output->{success} = 1;
134 2         5 $output->{input}->{command} = $self->{command};
135 2         5 $output->{input}->{description} = $self->{description};
136 2         4 $output->{input}->{files} = $self->{input_files};
137              
138             # Get cache
139 2         9 my ($cache, $cache_file) = _get_cache($self);
140 2 50 33     15 if (! $self->{no_cache} and defined $cache) {
141 0         0 return $cache;
142             }
143 2         5 my $stderr_file = "$cache_file.stderr";
144 2 50 33     6 if (defined $self->{save_stderr} and $self->{save_stderr} ne "1") {
145 0         0 $stderr_file = $self->{save_stderr};
146             }
147              
148             # Check input files
149 2         5 my $check_input = $self->_check_input_files_exist;
150              
151             # COMMAND EXECUTION
152 2         59 my $cmd = $self->command;
153 2 50       14 $cmd .= qq( 2> "$stderr_file") if (defined $self->{save_stderr});
154              
155 2         4937 my $command_output = `$cmd`;
156 2         54 $output->{output} = $command_output;
157 2         36 $output->{exit_code} = $?;
158              
159             # Check exit status
160 2 50 66     61 if ( $self->{die_on_error} and $output->{exit_code} ) {
161 0         0 confess("ERROR EXECUTING COMMAND ". $self->{description} . " \nCommand returned <" . $output->{exit_code} . ">:\n<" . $output->{input}->{command} .">");
162             }
163 2 50 33     30 $output->{stderr} = read_file("$stderr_file") if (defined $self->{save_stderr} and $self->{save_stderr} ne '1');
164              
165             # Check input output files
166 2         44 my $check_output = $self->_check_output_files_exist;
167              
168             # Save cache
169 2 50       15 _save_cache($self, $output) if (!$self->{no_cache});
170              
171 2 50 66     997 if ($self->{die_on_error} and ! $output->{success}) {
172             confess($output->{message})
173 0         0 } else {
174              
175 2         41 return $output;
176             }
177              
178             }
179             sub _error_header {
180 0     0   0 my ($command_string) = @_;
181              
182 0         0 return "<Error when executing command '$command_string'>\n";
183             }
184              
185             sub _md5 {
186 0     0   0 my ($file) = @_;
187 0         0 my $checksum = Digest::MD5->new;
188 0   0     0 open my $fh, '<', "$file" || confess("Unable to read file <$file> to calculate it's MD5 checksum.");
189 0         0 binmode($fh);
190              
191 0         0 while (<$fh>) {
192 0         0 $checksum->add($_);
193             }
194 0         0 close($fh);
195 0         0 return $checksum->hexdigest;
196              
197             }
198              
199              
200              
201              
202             sub _get_cache {
203 2     2   5 my ($self) = @_;
204              
205 2         18 my $md5 = Digest::MD5->new;
206 2 50       9 my $WD = defined $self->{working_dir} ? $self->{working_dir} : $GLOBAL{'working_dir'};
207              
208 2         11 $md5->add($self->{command}, $self->{description}, $WD);
209 2         9 my $md5sum = $md5->hexdigest;
210 2         7 my $cache_file = $WD . '/.' . $md5sum;
211 2         3 my $output;
212 2         10 $self->{cache_file} = $cache_file;
213 2 50       74 if (-e "$cache_file") {
214 0         0 eval { $output = lock_retrieve($cache_file); };
  0         0  
215 0 0       0 if ($@) {
216 0         0 _verbose($self, "Cache file found <$cache_file> but corrupted: skipping");
217 0         0 return (undef, $cache_file);
218             } else {
219 0         0 return ($output, $cache_file);
220             }
221             } else {
222 2         16 return (undef, $cache_file);
223             }
224             }
225              
226             sub _save_cache {
227 2     2   6 my ($self, $data) = @_;
228 2 50       15 confess("Hey, where is your filename?") unless (defined $self->{cache_file});
229 2         37 lock_store($data, $self->{cache_file});
230             }
231              
232              
233             sub _debug {
234              
235 0     0     my ($self, $message) = @_;
236 0 0         return 0 if ($self->{debug} < 1);
237 0           say STDERR "[Debug] ", $message;
238             }
239              
240             sub _verbose {
241 0     0     my ($self, $message) = @_;
242 0 0 0       if ($self->{verbose} > 0 or $self->{debug} > 0) {
243 0           say STDERR "[Info] ", $message;
244             }
245             }
246             sub get_global {
247 0     0 1   my ($self, $key) = @_;
248 0 0         if (defined $GLOBAL{$key}) {
249 0           return $GLOBAL{$key};
250             } else {
251 0           _verbose("Value not found for setting key <$key>");
252 0           return '<undef>';
253             }
254             }
255             sub set_global {
256 0     0 1   my ($self, $key, $value) = @_;
257 0 0         if (defined $GLOBAL{$key}) {
258 0           $GLOBAL{$key} = $value;
259 0           $self->{$key} = $value;
260 0           _debug($self, "Setting $key -> $value");
261             } else {
262 0           confess("Error setting <$key>: this is not a valid property");
263             }
264             }
265              
266              
267              
268             1;
269              
270             __END__
271              
272             =pod
273              
274             =encoding UTF-8
275              
276             =head1 NAME
277              
278             Proch::Cmd - Execute shell commands with caching capability to store output of executed programs (useful for multi step pipelines where some steps can take long) {beta}
279              
280             =head1 VERSION
281              
282             version 0.007
283              
284             =head1 SYNOPSIS
285              
286             use Proch::Cmd;
287              
288              
289             # The module is designed with settings affecting every execution
290             my $settings = Proch::Cmd->new(
291             command => '',
292             verbose => 1,
293             debug => 1
294             );
295              
296             # Settings can be edited at any time
297             $settings->set_global('working_dir', '/hpc-home/telatina/tmp/');
298              
299             # Create a new command object
300             my $c1 = Proch::Cmd->new(
301             command => 'ls -lh /etc/passwd /etc/vimrc hello',
302             input_files => ['/etc/passwd' , '/etc/vimrc', 'hello'],
303             output_files => [],
304             debug => 0,
305             verbose => 0,
306             object => \$object,
307             );
308              
309             my $simple = $c1->simplerun();
310              
311             say $simple->{output} if (! $simple->{exit_code});
312              
313             =head1 NAME
314              
315             Proch::Cmd - Execute shell commands controlling inputs and outputs
316              
317             =head1 VERSION
318              
319             version 0.0041
320              
321             =head1 METHODS
322              
323             =head2 new()
324              
325             The method creates a new shell command object, with the followin properties:
326              
327             =over 4
328              
329             =item I<command> [required]
330              
331             The shell command to execute
332              
333             =item I<workingdir> (default: /tmp) [important]
334              
335             Command temporary directory, should be the pipeline output directory, can be
336             omitted for minor commands like 'mkdir', but should be set for pipeline steps.
337              
338             =item I<description>
339              
340             Optional description of the command, for log and verbose mode
341              
342             =item I<input_files> (array)
343              
344             A list of files that must exist and be not empty before command execution
345              
346             =item I<output_files> (array)
347              
348             A list of files that must exist and be not empty after command execution
349              
350             =item I<die_on_error> (default: 1)
351              
352             If command returns non zero value, die (default behaviour)
353              
354             =item I<verbose>
355              
356             Enable verbose execution
357              
358             =item I<no_cache>
359              
360             Don't skip command execution if the command was already executed
361              
362             =back
363              
364             =head2 simplerun()
365              
366             Executes the shell command returning an object.
367              
368             =head2 get_global('Attribute')
369              
370             Returns the value of a setting given its name ('Attribute')
371              
372             =head2 set_global('Attribute', 'Value')
373              
374             Set the value of a setting item, given its name ('Attribute')
375              
376             =head1 ACCESSORY SCRIPTS
377              
378             The 'scripts' directory contain a I<read_cache_files.pl> that can be used to display the
379             content of this module's cache files. The 'data' directory contain a valid example of data
380             file called 'data.ok'. To view its content:
381              
382             perl scripts/read_cache_files.pl -f data/data.ok
383              
384             =head1 AUTHOR
385              
386             Andrea Telatin <andrea.telatin@quadram.ac.uk>
387              
388             =head1 COPYRIGHT AND LICENSE
389              
390             This software is Copyright (c) 2019 by Andrea Telatin.
391              
392             This is free software, licensed under:
393              
394             The MIT (X11) License
395              
396             =cut