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   92064 use strict;
  15         47  
  15         418  
2 15     15   65 use warnings;
  15         23  
  15         685  
3              
4             package Footprintless::Command;
5             $Footprintless::Command::VERSION = '1.29';
6             # ABSTRACT: A factory for building common commands
7             # PODNAME: Footprintless::Command
8              
9 15     15   73 use Exporter qw(import);
  15         26  
  15         752  
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   80 use File::Spec;
  15         25  
  15         36449  
24              
25             sub batch_command {
26 35     35 1 10189 my ( @commands, $batch_options, $command_options );
27 35         86 (@commands) = @_;
28 35 100       122 $command_options = pop(@commands)
29             if ( ref( $commands[$#commands] ) eq 'Footprintless::Command::CommandOptions' );
30 35 100       112 $batch_options = pop(@commands) if ( ref( $commands[$#commands] ) eq 'HASH' );
31              
32 35 100       90 push( @commands, $command_options ) if ($command_options);
33              
34             wrap(
35             $batch_options || {},
36             @commands,
37             sub {
38 35     35   89 return @_;
39             }
40 35   100     322 );
41             }
42              
43             sub command {
44             wrap(
45             {},
46             @_,
47             sub {
48 93     93   160 return shift;
49             }
50 93     93 1 16560 );
51             }
52              
53             sub command_options {
54 83     83 1 2330 return Footprintless::Command::CommandOptions->new(@_);
55             }
56              
57             sub cp_command {
58 14     14 1 21 my ( $source_path, $source_command_options, $destination_path, $destination_command_options,
59             %cp_options );
60 14         25 $source_path = _escape_path(shift);
61 14 100       35 $source_command_options = shift
62             if ( ref( $_[0] ) eq 'Footprintless::Command::CommandOptions' );
63 14         22 $destination_path = _escape_path(shift);
64 14 100       24 $destination_command_options = shift
65             if ( ref( $_[0] ) eq 'Footprintless::Command::CommandOptions' );
66 14         31 %cp_options = @_;
67              
68 14 100       26 $source_command_options = command_options() unless ($source_command_options);
69 14 100       26 $destination_command_options = command_options() unless ($destination_command_options);
70              
71 14         22 my $source_command;
72             my $destination_command;
73 14 100       25 if ( $cp_options{file} ) {
74              
75             # is a file, so use cat | dd
76 6 100       10 if ( $cp_options{compress} ) {
77 1         5 $source_command = command( "gzip -c $source_path", $source_command_options );
78 1         5 $destination_command =
79             pipe_command( "gunzip", "dd of=$destination_path", $destination_command_options );
80             }
81             else {
82 5         12 $source_command = command( "cat $source_path", $source_command_options );
83 5         18 $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     26 if ( $cp_options{archive} && $cp_options{archive} eq 'zip' ) {
90             my $temp_zip = File::Spec->catfile( $destination_path,
91 3   50     41 $cp_options{unzip_temp_file} || "temp_cp_command.zip" );
92 3         15 $source_command =
93             command(
94             batch_command( "cd $source_path", "zip -qr - .", { subshell => "bash -c " } ),
95             $source_command_options );
96 3         17 $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         12 my @parts = ("tar -c -C $source_path .");
104 5         8 my @destination_parts = ();
105 5 100       9 if ( $cp_options{status} ) {
106 1 50       12 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       13 if ( $cp_options{compress} ) {
124 2         4 push( @parts, 'gzip' );
125 2         2 push( @destination_parts, 'gunzip' );
126             }
127             push(
128 5 50       13 @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         11 $source_command = command( pipe_command(@parts), $source_command_options );
140 5         19 $destination_command = command( pipe_command(@destination_parts),
141             $destination_command_options->clone( sudo_username => undef ) );
142             }
143             }
144              
145 14         52 return pipe_command( $source_command, $destination_command );
146             }
147              
148             sub _escape_path {
149 28     28   34 my ($path) = @_;
150 28         60 $path =~ s/(['"`\s])/\\$1/g;
151 28         46 return $path;
152             }
153              
154             sub mkdir_command {
155             wrap(
156             {},
157             @_,
158             sub {
159 15     15   68 return 'mkdir -p "' . join( '" "', @_ ) . '"';
160             }
161 15     15 1 97 );
162             }
163              
164             sub pipe_command {
165             wrap(
166             { command_separator => '|' },
167             @_,
168             sub {
169 33     33   54 return @_;
170             }
171 33     33 1 114 );
172             }
173              
174             sub _quote_command {
175 59     59   91 my ($command) = @_;
176 59         99 $command =~ s/\\/\\\\/g;
177 59         75 $command =~ s/\$/\\\$/g;
178 59         76 $command =~ s/`/\\`/g; # for `command`
179 59         98 $command =~ s/"/\\"/g;
180 59         139 return "\"$command\"";
181             }
182              
183             sub rm_command {
184             Footprintless::Command::wrap(
185             {},
186             @_,
187             sub {
188 23     23   35 my ( @dirs, @files );
189 23         62 foreach my $entry (@_) {
190 34 100       181 if ( $entry =~ /(?:\/|\\)$/ ) {
191 23         62 push( @dirs, $entry );
192             }
193             else {
194 11         22 push( @files, $entry );
195             }
196             }
197              
198 23 100       61 if (@dirs) {
199 17 100       40 if (@files) {
200 2         14 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         118 return 'rm -rf "' . join( '" "', sort(@dirs) ) . '"';
208             }
209             }
210             else {
211 6         26 return 'rm -f "' . join( '" "', sort(@files) ) . '"';
212             }
213             }
214 23     23 1 249 );
215             }
216              
217             sub sed_command {
218             wrap(
219             {},
220             @_,
221             sub {
222 2     2   4 my @args = @_;
223 2         4 my $options = {};
224              
225 2 100       6 if ( ref( $args[$#args] ) eq 'HASH' ) {
226 1         2 $options = pop(@args);
227             }
228              
229 2         4 my $command = 'sed';
230 2 50       7 $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       8 $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         5 keys( %{ $options->{replace_map} } ) )
250 2 100       7 if ( defined( $options->{replace_map} ) );
251             }
252 2 50       6 $command .= join( ' ', '', @{ $options->{files} } ) if ( $options->{files} );
  0         0  
253              
254 2         4 return $command;
255             }
256 2     2 1 14 );
257             }
258              
259             sub _sudo_command {
260 295     295   482 my ( $sudo_command, $sudo_username, $command ) = @_;
261 295 100       487 if ( defined($sudo_username) ) {
262 49 100       144 $command =
    100          
263             ( $sudo_command ? "$sudo_command " : 'sudo ' )
264             . ( $sudo_username ? "-u $sudo_username " : '' )
265             . $command;
266             }
267 295         447 return $command;
268             }
269              
270             sub tail_command {
271             Footprintless::Command::wrap(
272             {},
273             @_,
274             sub {
275 10     10   32 my ( $file, %options ) = @_;
276 10         25 my @command = ('tail');
277 10 100       31 if ( $options{follow} ) {
    50          
278 9         23 push( @command, '-f' );
279             }
280             elsif ( $options{lines} ) {
281 1         2 push( @command, '-n', $options{lines} );
282             }
283 10         15 push( @command, $file );
284 10         50 return join( ' ', @command );
285             }
286 10     10 1 80 );
287             }
288              
289             sub write_command {
290 5     5 1 8 my ( $filename, @lines, $write_options, $command_options );
291 5         7 $filename = shift;
292 5         11 @lines = @_;
293 5 100       16 $command_options = pop(@lines)
294             if ( ref( $lines[$#lines] ) eq 'Footprintless::Command::CommandOptions' );
295 5 100       12 $write_options = pop(@lines) if ( ref( $lines[$#lines] ) eq 'HASH' );
296              
297 5         11 my $remote_command = "dd of=$filename";
298 5 100 66     19 if ( defined($write_options) && defined( $write_options->{mode} ) ) {
    100          
299 3 50       5 if ( defined($command_options) ) {
    0          
300 3         12 $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         4 $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     26 : '\n';
317 5         17 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 348 my $wrap_options = shift;
323 211         298 my $builder = pop;
324 211         400 my @args = @_;
325 211         296 my ( $ssh, $username, $hostname, $sudo_command, $sudo_username, $pretty );
326              
327 211 100       546 if ( ref( $args[$#args] ) eq 'Footprintless::Command::CommandOptions' ) {
328 121         168 my $options = pop(@args);
329 121   100     290 $ssh = $options->get_ssh() || 'ssh';
330 121         221 $username = $options->get_username();
331 121         228 $hostname = $options->get_hostname();
332 121         222 $sudo_command = $options->get_sudo_command();
333 121         209 $sudo_username = $options->get_sudo_username();
334 121         297 $pretty = $options->get_pretty();
335             }
336              
337 211         355 my $destination_command = '';
338 211   100     655 my $command_separator = $wrap_options->{command_separator} || ';';
339 211         290 my $commands = 0;
340 211         385 foreach my $command ( &$builder(@args) ) {
341 290 100       513 if ( defined($command) ) {
342 289 100       524 if ( $commands++ > 0 ) {
343 78         90 $destination_command .= $command_separator;
344 78 50       131 if ($pretty) {
345 0         0 $destination_command .= "\n";
346             }
347             }
348              
349 289         496 $command =~ s/^(.*?[^\\]);$/$1/; # from find -exec
350              
351 289         598 $command = _sudo_command( $sudo_command, $sudo_username, $command );
352              
353 289         674 $destination_command .= $command;
354             }
355             }
356              
357 211 100       419 if ( $wrap_options->{subshell} ) {
358 7         15 $destination_command = $wrap_options->{subshell} . _quote_command($destination_command);
359             }
360              
361 211 100 100     690 if ( !defined($username) && !defined($hostname) ) {
362              
363             # silly to ssh to localhost as current user, so dont
364 159         874 return $destination_command;
365             }
366              
367 52 100       100 my $userAt =
    100          
368             $username
369             ? ( ( $ssh =~ /plink(?:\.exe)?$/ ) ? "-l $username " : "$username\@" )
370             : '';
371              
372 52         92 $destination_command = _quote_command($destination_command);
373 52   50     297 return "$ssh $userAt" . ( $hostname || 'localhost' ) . " $destination_command";
374             }
375              
376             package Footprintless::Command::CommandOptions;
377             $Footprintless::Command::CommandOptions::VERSION = '1.29';
378             sub new {
379 92     92   306 return bless( {}, shift )->_init(@_);
380             }
381              
382             sub clone {
383 9     9   24 my ( $instance, %options ) = @_;
384              
385 9 100 66     30 if ( exists( $instance->{hostname} ) && !exists( $options{hostname} ) ) {
386 3         4 $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     55 if ( exists( $instance->{username} ) && !exists( $options{username} ) ) {
392 0         0 $options{username} = $instance->{username};
393             }
394 9 50 33     22 if ( exists( $instance->{sudo_command} ) && !exists( $options{sudo_command} ) ) {
395 0         0 $options{sudo_command} = $instance->{sudo_command};
396             }
397 9 50 66     55 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   175 return $_[0]->{hostname};
409             }
410              
411             sub get_pretty {
412 121     121   187 return $_[0]->{pretty};
413             }
414              
415             sub get_ssh {
416 121     121   338 return $_[0]->{ssh};
417             }
418              
419             sub get_sudo_command {
420 127     127   179 return $_[0]->{sudo_command};
421             }
422              
423             sub get_sudo_username {
424 127     127   179 return $_[0]->{sudo_username};
425             }
426              
427             sub get_username {
428 121     121   182 return $_[0]->{username};
429             }
430              
431             sub _init {
432 92     92   288 my ( $self, %options ) = @_;
433              
434 92 100       247 $self->{hostname} = $options{hostname} if ( defined( $options{hostname} ) );
435 92 100       280 $self->{ssh} = $options{ssh} if ( defined( $options{ssh} ) );
436 92 100       186 $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       178 $self->{sudo_username} = $options{sudo_username} if ( defined( $options{sudo_username} ) );
439 92 50       167 $self->{pretty} = $options{pretty} if ( defined( $options{pretty} ) );
440              
441 92         429 return $self;
442             }
443              
444             __END__