File Coverage

blib/lib/Proch/Cmd.pm
Criterion Covered Total %
statement 91 140 65.0
branch 12 38 31.5
condition 7 27 25.9
subroutine 16 23 69.5
pod 1 3 33.3
total 127 231 54.9


line stmt bran cond sub pod time code
1 2     2   1160 use strict;
  2         9  
  2         57  
2 2     2   9 use warnings;
  2         4  
  2         79  
3             package Proch::Cmd;
4              
5             $Proch::Cmd::VERSION = 0.0041;
6             # ABSTRACT: Execute shell commands controlling inputs and outputs
7              
8              
9 2     2   38 use 5.014;
  2         7  
10 2     2   1088 use Moose;
  2         938662  
  2         14  
11 2     2   16631 use Data::Dumper;
  2         13950  
  2         150  
12 2     2   16 use Digest::MD5;
  2         4  
  2         73  
13 2     2   1277 use Storable qw(lock_store lock_nstore lock_retrieve);
  2         6857  
  2         154  
14 2     2   15 use Carp qw(confess);
  2         5  
  2         100  
15 2     2   1131 use File::Slurp;
  2         61995  
  2         138  
16 2     2   1116 use Time::HiRes qw(gettimeofday tv_interval);
  2         2768  
  2         8  
17 2     2   1362 use Time::Piece;
  2         18599  
  2         8  
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 1     1   3 my ($self) = @_;
80 1         2 my @msg = ();
81 1         2 my $errors = 0;
82 1         2 my $output;
83            
84             # Check input input_files
85 1         1 foreach my $file (@{ $self->{input_files} }) {
  1         6  
86 0 0       0 if (! -s "$file") {
87 0         0 $errors++;
88 0         0 push(@msg, qq("$file") );
89             }
90             }
91              
92 1         5 $output->{errors} = $errors;
93 1         3 $output->{raw_messages} = \@msg;
94 1         6 $output->{message} = "Required INPUT file not found: [" . join(', ', @msg) ."] when executing <" .$self->{command} .'>';
95              
96 1 50 33     6 if ($self->{die_on_error} and $output->{errors}){
97 0         0 confess($output->{message});
98             } else {
99 1         3 return $output;
100             }
101             }
102             sub _check_output_files_exist {
103             # All {input_files} need to be found
104 1     1   14 my ($self) = @_;
105 1         7 my @msg = ();
106 1         3 my $errors = 0;
107 1         7 my $output;
108            
109             # Check input input_files
110 1         9 foreach my $file (@{ $self->{output_files} }) {
  1         24  
111 0 0       0 if (! -s "$file") {
112 0         0 $errors++;
113 0         0 push(@msg, qq("$file") );
114             }
115             }
116              
117 1         12 $output->{errors} = $errors;
118 1         14 $output->{raw_messages} = \@msg;
119 1         20 $output->{message} = "Required OUTPUT file not found: [" . join(', ', @msg) ."] when executing <" .$self->{command} .'>';
120              
121 1 50 33     33 if ($self->{die_on_error} and $output->{errors}){
122 0         0 confess($output->{message});
123             } else {
124 1         9 return $output;
125             }
126             }
127             sub simplerun {
128 1     1 1 3346 my ($self) = @_;
129 1         6 my $start_date = localtime->strftime('%m/%d/%Y %H:%M');
130 1         189 my $start_time = [gettimeofday];
131 1         2 my $output;
132 1         5 $output->{success} = 1;
133 1         3 $output->{input}->{command} = $self->{command};
134 1         3 $output->{input}->{description} = $self->{description};
135 1         3 $output->{input}->{files} = $self->{input_files};
136              
137             # Get cache
138 1         4 my ($cache, $cache_file) = _get_cache($self);
139 1 50 33     12 if (! $self->{no_cache} and defined $cache) {
140 0         0 return $cache;
141 0         0 confess();
142             }
143 1         4 my $stderr_file = "$cache_file.stderr";
144 1 50 33     5 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 1         4 my $check_input = $self->_check_input_files_exist;
150            
151             # COMMAND EXECUTION
152 1         35 my $cmd = $self->command;
153 1 50       5 $cmd .= qq( 2> "$stderr_file") if (defined $self->{save_stderr});
154              
155 1         4138 my $command_output = `$cmd`;
156 1         38 $output->{output} = $command_output;
157 1         27 $output->{exit_code} = $?;
158              
159             # Check exit status
160 1 50 33     60 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 1 50 33     21 $output->{stderr} = read_file("$stderr_file") if (defined $self->{save_stderr} and $self->{save_stderr} ne '1');
164              
165             # Check input output files
166 1         29 my $check_output = $self->_check_output_files_exist;
167              
168             # Save cache
169 1 50       26 _save_cache($self, $output) if (!$self->{no_cache});
170            
171 1 50 33     949 if ($self->{die_on_error} and ! $output->{success}) {
172             confess($output->{message})
173 0         0 } else {
174              
175 1         28 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 1     1   2 my ($self) = @_;
204            
205 1         19 my $md5 = Digest::MD5->new;
206 1 50       5 my $WD = defined $self->{working_dir} ? $self->{working_dir} : $GLOBAL{'working_dir'};
207              
208 1         6 $md5->add($self->{command}, $self->{description}, $WD);
209 1         4 my $md5sum = $md5->hexdigest;
210 1         5 my $cache_file = $WD . '/.' . $md5sum;
211 1         2 my $output;
212 1         2 $self->{cache_file} = $cache_file;
213 1 50       62 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 1         10 return (undef, $cache_file);
223             }
224             }
225              
226             sub _save_cache {
227 1     1   11 my ($self, $data) = @_;
228 1 50       6 confess("Hey, where is your filename?") unless (defined $self->{cache_file});
229 1         28 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 0   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 0   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 controlling inputs and outputs
279              
280             =head1 VERSION
281              
282             version 0.0041
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 - a simple library to execute shell commands
316              
317             =head1 VERSION
318              
319             version 0.004
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             =head1 ACCESSORY SCRIPTS
369              
370             The 'scripts' directory contain a I<read_cache_files.pl> that can be used to display the
371             content of this module's cache files. The 'data' directory contain a valid example of data
372             file called 'data.ok'. To view its content:
373              
374             perl scripts/read_cache_files.pl -f data/data.ok
375              
376             =head1 AUTHOR
377              
378             Andrea Telatin <andrea@telatin.com>
379              
380             =head1 COPYRIGHT AND LICENSE
381              
382             This software is free software under MIT Licence.
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