File Coverage

blib/lib/Footprintless/Command.pm
Criterion Covered Total %
statement 169 187 90.3
branch 94 114 82.4
condition 27 40 67.5
subroutine 34 34 100.0
pod 10 11 90.9
total 334 386 86.5


line stmt bran cond sub pod time code
1 15     15   71727 use strict;
  15         44  
  15         383  
2 15     15   76 use warnings;
  15         31  
  15         642  
3              
4             package Footprintless::Command;
5             $Footprintless::Command::VERSION = '1.26';
6             # ABSTRACT: A factory for building common commands
7             # PODNAME: Footprintless::Command
8              
9 15     15   71 use Exporter qw(import);
  15         27  
  15         703  
10             our @EXPORT_OK = qw(
11             batch_command
12             command
13             command_options
14             cp_command
15             mkdir_command
16             pipe_command
17             rm_command
18             sed_command
19             tail_command
20             write_command
21             );
22              
23 15     15   73 use File::Spec;
  15         23  
  15         30487  
24              
25             sub batch_command {
26 35     35 1 8836 my ( @commands, $batch_options, $command_options );
27 35         103 (@commands) = @_;
28 35 100       142 $command_options = pop(@commands)
29             if ( ref( $commands[$#commands] ) eq 'Footprintless::Command::CommandOptions' );
30 35 100       115 $batch_options = pop(@commands) if ( ref( $commands[$#commands] ) eq 'HASH' );
31              
32 35 100       98 push( @commands, $command_options ) if ($command_options);
33              
34             wrap(
35             $batch_options || {},
36             @commands,
37             sub {
38 35     35   93 return @_;
39             }
40 35   100     291 );
41             }
42              
43             sub command {
44             wrap(
45             {},
46             @_,
47             sub {
48 93     93   199 return shift;
49             }
50 93     93 1 422 );
51             }
52              
53             sub command_options {
54 83     83 1 428 return Footprintless::Command::CommandOptions->new(@_);
55             }
56              
57             sub cp_command {
58 14     14 1 33 my ( $source_path, $source_command_options, $destination_path, $destination_command_options,
59             %cp_options );
60 14         34 $source_path = _escape_path(shift);
61 14 100       49 $source_command_options = shift
62             if ( ref( $_[0] ) eq 'Footprintless::Command::CommandOptions' );
63 14         31 $destination_path = _escape_path(shift);
64 14 100       39 $destination_command_options = shift
65             if ( ref( $_[0] ) eq 'Footprintless::Command::CommandOptions' );
66 14         39 %cp_options = @_;
67              
68 14 100       40 $source_command_options = command_options() unless ($source_command_options);
69 14 100       34 $destination_command_options = command_options() unless ($destination_command_options);
70              
71 14         26 my $source_command;
72             my $destination_command;
73 14 100       36 if ( $cp_options{file} ) {
74              
75             # is a file, so use cat | dd
76 6 100       18 if ( $cp_options{compress} ) {
77 1         9 $source_command = command( "gzip -c $source_path", $source_command_options );
78 1         10 $destination_command =
79             pipe_command( "gunzip", "dd of=$destination_path", $destination_command_options );
80             }
81             else {
82 5         17 $source_command = command( "cat $source_path", $source_command_options );
83 5         32 $destination_command =
84             command( "dd of=$destination_path", $destination_command_options );
85             }
86             }
87             else {
88             # is a directory, so use tar or unzip
89 8 100 66     30 if ( $cp_options{archive} && $cp_options{archive} eq 'zip' ) {
90             my $temp_zip = File::Spec->catfile( $destination_path,
91 3   50     52 $cp_options{unzip_temp_file} || "temp_cp_command.zip" );
92 3         16 $source_command =
93             command(
94             batch_command( "cd $source_path", "zip -qr - .", { subshell => "bash -c " } ),
95             $source_command_options );
96 3         23 $destination_command = batch_command(
97             "dd of=$temp_zip", "unzip -qod $destination_path $temp_zip",
98             rm_command($temp_zip), $destination_command_options
99             );
100             }
101             else {
102             # default, use tar
103 5         19 my @parts = ("tar -c -C $source_path .");
104 5         10 my @destination_parts = ();
105 5 100       13 if ( $cp_options{status} ) {
106 1 50       21 push(
107             @parts,
108             join(
109             '',
110             'pv -f -s `',
111             _sudo_command(
112             $source_command_options
113             ? ( $source_command_options->get_sudo_command(),
114             $source_command_options->get_sudo_username()
115             )
116             : ( undef, undef ),
117             pipe_command( "du -sb $source_path", 'cut -f1' )
118             ),
119             '`'
120             )
121             );
122             }
123 5 100       18 if ( $cp_options{compress} ) {
124 2         5 push( @parts, 'gzip' );
125 2         5 push( @destination_parts, 'gunzip' );
126             }
127             push(
128 5 50       18 @destination_parts,
129             _sudo_command(
130             $destination_command_options
131             ? ( $destination_command_options->get_sudo_command(),
132             $destination_command_options->get_sudo_username()
133             )
134             : ( undef, undef ),
135             "tar --no-overwrite-dir -x -C $destination_path"
136             )
137             );
138              
139 5         13 $source_command = command( pipe_command(@parts), $source_command_options );
140 5         25 $destination_command = command( pipe_command(@destination_parts),
141             $destination_command_options->clone( sudo_username => undef ) );
142             }
143             }
144              
145 14         92 return pipe_command( $source_command, $destination_command );
146             }
147              
148             sub _escape_path {
149 28     28   55 my ($path) = @_;
150 28         90 $path =~ s/(['"`\s])/\\$1/g;
151 28         59 return $path;
152             }
153              
154             sub mkdir_command {
155             wrap(
156             {},
157             @_,
158             sub {
159 15     15   77 return 'mkdir -p "' . join( '" "', @_ ) . '"';
160             }
161 15     15 1 91 );
162             }
163              
164             sub pipe_command {
165             wrap(
166             { command_separator => '|' },
167             @_,
168             sub {
169 33     33   78 return @_;
170             }
171 33     33 1 178 );
172             }
173              
174             sub _quote_command {
175 59     59   118 my ($command) = @_;
176 59         114 $command =~ s/\\/\\\\/g;
177 59         94 $command =~ s/\$/\\\$/g;
178 59         115 $command =~ s/`/\\`/g; # for `command`
179 59         121 $command =~ s/"/\\"/g;
180 59         169 return "\"$command\"";
181             }
182              
183             sub rm_command {
184             Footprintless::Command::wrap(
185             {},
186             @_,
187             sub {
188 23     23   44 my ( @dirs, @files );
189 23         67 foreach my $entry (@_) {
190 34 100       183 if ( $entry =~ /(?:\/|\\)$/ ) {
191 23         68 push( @dirs, $entry );
192             }
193             else {
194 11         27 push( @files, $entry );
195             }
196             }
197              
198 23 100       70 if (@dirs) {
199 17 100       48 if (@files) {
200 2         29 return batch_command(
201             'rm -rf "' . join( '" "', sort(@dirs) ) . '"',
202             'rm -f "' . join( '" "', sort(@files) ) . '"',
203             { subshell => 'bash -c ' }
204             );
205             }
206             else {
207 15         130 return 'rm -rf "' . join( '" "', sort(@dirs) ) . '"';
208             }
209             }
210             else {
211 6         39 return 'rm -f "' . join( '" "', sort(@files) ) . '"';
212             }
213             }
214 23     23 1 217 );
215             }
216              
217             sub sed_command {
218             wrap(
219             {},
220             @_,
221             sub {
222 2     2   6 my @args = @_;
223 2         4 my $options = {};
224              
225 2 100       7 if ( ref( $args[$#args] ) eq 'HASH' ) {
226 1         2 $options = pop(@args);
227             }
228              
229 2         6 my $command = 'sed';
230 2 50       8 $command .= ' -i' if ( $options->{in_place} );
231 2 50       7 if ( defined( $options->{temp_script_file} ) ) {
232 0         0 my $temp_script_file_name = $options->{temp_script_file}->filename();
233 0 0       0 print( { $options->{temp_script_file} } join( ' ', '', map {"$_;"} @args ) )
  0         0  
  0         0  
234             if ( scalar(@args) );
235             print(
236 0         0 { $options->{temp_script_file} } join( ' ',
237             '',
238 0         0 map {"s/$_/$options->{replace_map}{$_}/g;"}
239 0         0 keys( %{ $options->{replace_map} } ) )
240 0 0       0 ) if ( defined( $options->{replace_map} ) );
241 0         0 $options->{temp_script_file}->flush();
242 0         0 $command .= " -f $temp_script_file_name";
243             }
244             else {
245 2 100       7 $command .= join( ' ', '', map {"-e '$_'"} @args ) if ( scalar(@args) );
  1         6  
246             $command .= join( ' ',
247             '',
248 1         7 map {"-e 's/$_/$options->{replace_map}{$_}/g'"}
249 1         4 keys( %{ $options->{replace_map} } ) )
250 2 100       8 if ( defined( $options->{replace_map} ) );
251             }
252 2 50       6 $command .= join( ' ', '', @{ $options->{files} } ) if ( $options->{files} );
  0         0  
253              
254 2         6 return $command;
255             }
256 2     2 1 18 );
257             }
258              
259             sub _sudo_command {
260 295     295   545 my ( $sudo_command, $sudo_username, $command ) = @_;
261 295 100       511 if ( defined($sudo_username) ) {
262 49 100       192 $command =
    100          
263             ( $sudo_command ? "$sudo_command " : 'sudo ' )
264             . ( $sudo_username ? "-u $sudo_username " : '' )
265             . $command;
266             }
267 295         539 return $command;
268             }
269              
270             sub tail_command {
271             Footprintless::Command::wrap(
272             {},
273             @_,
274             sub {
275 10     10   40 my ( $file, %options ) = @_;
276 10         25 my @command = ('tail');
277 10 100       32 if ( $options{follow} ) {
    50          
278 9         16 push( @command, '-f' );
279             }
280             elsif ( $options{lines} ) {
281 1         3 push( @command, '-n', $options{lines} );
282             }
283 10         20 push( @command, $file );
284 10         59 return join( ' ', @command );
285             }
286 10     10 1 88 );
287             }
288              
289             sub write_command {
290 5     5 1 11 my ( $filename, @lines, $write_options, $command_options );
291 5         11 $filename = shift;
292 5         15 @lines = @_;
293 5 100       20 $command_options = pop(@lines)
294             if ( ref( $lines[$#lines] ) eq 'Footprintless::Command::CommandOptions' );
295 5 100       17 $write_options = pop(@lines) if ( ref( $lines[$#lines] ) eq 'HASH' );
296              
297 5         14 my $remote_command = "dd of=$filename";
298 5 100 66     26 if ( defined($write_options) && defined( $write_options->{mode} ) ) {
    100          
299 3 50       9 if ( defined($command_options) ) {
    0          
300 3         13 $remote_command =
301             batch_command( $remote_command, "chmod $write_options->{mode} $filename",
302             $command_options );
303             }
304             elsif ( defined($command_options) ) {
305 0         0 $remote_command =
306             batch_command( $remote_command, "chmod $write_options->{mode} $filename" );
307             }
308             }
309             elsif ( defined($command_options) ) {
310 1         6 $remote_command = command( $remote_command, $command_options );
311             }
312              
313             my $line_separator =
314             ( defined($write_options) && defined( $write_options->{line_separator} ) )
315             ? $write_options->{line_separator}
316 5 100 100     36 : '\n';
317 5         27 return pipe_command( 'printf "' . join( $line_separator, @lines ) . '"', $remote_command );
318             }
319              
320             # Handles wrapping commands with possible ssh and command prefix
321             sub wrap {
322 211     211 0 385 my $wrap_options = shift;
323 211         357 my $builder = pop;
324 211         442 my @args = @_;
325 211         353 my ( $ssh, $username, $hostname, $sudo_command, $sudo_username, $pretty );
326              
327 211 100       602 if ( ref( $args[$#args] ) eq 'Footprintless::Command::CommandOptions' ) {
328 121         184 my $options = pop(@args);
329 121   100     289 $ssh = $options->get_ssh() || 'ssh';
330 121         271 $username = $options->get_username();
331 121         274 $hostname = $options->get_hostname();
332 121         243 $sudo_command = $options->get_sudo_command();
333 121         216 $sudo_username = $options->get_sudo_username();
334 121         228 $pretty = $options->get_pretty();
335             }
336              
337 211         396 my $destination_command = '';
338 211   100     678 my $command_separator = $wrap_options->{command_separator} || ';';
339 211         315 my $commands = 0;
340 211         432 foreach my $command ( &$builder(@args) ) {
341 290 100       570 if ( defined($command) ) {
342 289 100       581 if ( $commands++ > 0 ) {
343 78         131 $destination_command .= $command_separator;
344 78 50       144 if ($pretty) {
345 0         0 $destination_command .= "\n";
346             }
347             }
348              
349 289         575 $command =~ s/^(.*?[^\\]);$/$1/; # from find -exec
350              
351 289         553 $command = _sudo_command( $sudo_command, $sudo_username, $command );
352              
353 289         695 $destination_command .= $command;
354             }
355             }
356              
357 211 100       491 if ( $wrap_options->{subshell} ) {
358 7         17 $destination_command = $wrap_options->{subshell} . _quote_command($destination_command);
359             }
360              
361 211 100 100     734 if ( !defined($username) && !defined($hostname) ) {
362              
363             # silly to ssh to localhost as current user, so dont
364 159         974 return $destination_command;
365             }
366              
367 52 100       135 my $userAt =
    100          
368             $username
369             ? ( ( $ssh =~ /plink(?:\.exe)?$/ ) ? "-l $username " : "$username\@" )
370             : '';
371              
372 52         108 $destination_command = _quote_command($destination_command);
373 52   50     364 return "$ssh $userAt" . ( $hostname || 'localhost' ) . " $destination_command";
374             }
375              
376             package Footprintless::Command::CommandOptions;
377             $Footprintless::Command::CommandOptions::VERSION = '1.26';
378             sub new {
379 92     92   321 return bless( {}, shift )->_init(@_);
380             }
381              
382             sub clone {
383 9     9   22 my ( $instance, %options ) = @_;
384              
385 9 100 66     26 if ( exists( $instance->{hostname} ) && !exists( $options{hostname} ) ) {
386 3         7 $options{hostname} = $instance->{hostname};
387             }
388 9 50 66     28 if ( exists( $instance->{ssh} ) && !exists( $options{ssh} ) ) {
389 0         0 $options{ssh} = $instance->{ssh};
390             }
391 9 50 33     48 if ( exists( $instance->{username} ) && !exists( $options{username} ) ) {
392 0         0 $options{username} = $instance->{username};
393             }
394 9 50 33     29 if ( exists( $instance->{sudo_command} ) && !exists( $options{sudo_command} ) ) {
395 0         0 $options{sudo_command} = $instance->{sudo_command};
396             }
397 9 50 66     43 if ( exists( $instance->{sudo_username} ) && !exists( $options{sudo_username} ) ) {
398 0         0 $options{sudo_username} = $instance->{sudo_username};
399             }
400 9 50 33     19 if ( exists( $instance->{pretty} ) && !exists( $options{pretty} ) ) {
401 0         0 $options{pretty} = $instance->{pretty};
402             }
403              
404 9         28 return new( ref($instance), %options );
405             }
406              
407             sub get_hostname {
408 121     121   220 return $_[0]->{hostname};
409             }
410              
411             sub get_pretty {
412 121     121   216 return $_[0]->{pretty};
413             }
414              
415             sub get_ssh {
416 121     121   388 return $_[0]->{ssh};
417             }
418              
419             sub get_sudo_command {
420 127     127   240 return $_[0]->{sudo_command};
421             }
422              
423             sub get_sudo_username {
424 127     127   226 return $_[0]->{sudo_username};
425             }
426              
427             sub get_username {
428 121     121   215 return $_[0]->{username};
429             }
430              
431             sub _init {
432 92     92   322 my ( $self, %options ) = @_;
433              
434 92 100       304 $self->{hostname} = $options{hostname} if ( defined( $options{hostname} ) );
435 92 100       282 $self->{ssh} = $options{ssh} if ( defined( $options{ssh} ) );
436 92 100       207 $self->{username} = $options{username} if ( defined( $options{username} ) );
437 92 100       194 $self->{sudo_command} = $options{sudo_command} if ( defined( $options{sudo_command} ) );
438 92 100       210 $self->{sudo_username} = $options{sudo_username} if ( defined( $options{sudo_username} ) );
439 92 50       195 $self->{pretty} = $options{pretty} if ( defined( $options{pretty} ) );
440              
441 92         484 return $self;
442             }
443              
444             __END__