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