File Coverage

bin/rm
Criterion Covered Total %
statement 124 143 86.7
branch 44 68 64.7
condition 11 12 91.6
subroutine 26 30 86.6
pod n/a
total 205 253 81.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =encoding utf8
4              
5             =begin metadata
6              
7             Name: rm
8             Description: remove directory entries
9             Author: brian d foy, bdfoy@cpan.org
10             License: artistic2
11              
12             =end metadata
13              
14             =cut
15              
16             =head1 NAME
17              
18             rm - remove directory entries
19              
20             =head1 SYNOPSIS
21              
22             rm [-fiPrR] file ...
23              
24             =head1 DESCRIPTION
25              
26             =head1 OPTIONS
27              
28             =over 4
29              
30             =item * -f - do not prompt the user for each file, and do not consider it an error if a file cannot be removed
31              
32             =item * -i - prompt the user for each file.
33              
34             =item * -P - a no-op, for compatibility. So implementations would overwrite files with random data
35              
36             =item * -r - same as -R
37              
38             =item * -R - remove directories recursively
39              
40             =item * -v
41              
42             =back
43              
44             =head1 AUTHOR
45              
46             Copyright (c) brian d foy, bdfoy@cpan.org
47              
48             The original version of this program was written by Steve Kemp,
49             steve@steve.org.uk, but almost none of that remains.
50              
51             =head1 LICENCE
52              
53             This program is licensed under the Artistic License 2.0.
54              
55             =cut
56              
57             package PerlPowerTools::rm;
58              
59 2     2   921 use strict;
  2         4  
  2         61  
60              
61 2     2   10 use File::Basename;
  2         3  
  2         127  
62 2     2   933 use File::Spec::Functions;
  2         1727  
  2         157  
63 2     2   3198 use Storable qw(dclone);
  2         7806  
  2         145  
64              
65 2     2   15 use constant EX_SUCCESS => 0;
  2         4  
  2         185  
66 2     2   15 use constant EX_FAILURE => 1;
  2         4  
  2         134  
67 2     2   11 use constant EX_USAGE => 2;
  2         3  
  2         104  
68 2     2   10 use constant OP_SUCCEEDED => 0;
  2         19  
  2         105  
69 2     2   10 use constant OP_FAILED => 1;
  2         3  
  2         4080  
70              
71             my $Program = basename($0);
72              
73             __PACKAGE__->run( args => \@ARGV ) unless caller;
74              
75             sub run {
76 10     10   100234 my $class = shift;
77 10         29 my %args = @_;
78              
79 10         18 my $args = delete $args{args};
80              
81             # This looks funny because the other args are filehandles, which
82             # we can't dupe. We want to play with the command-line args such
83             # that we don't mess up anything that called us.
84 10         580 my $self = $class->new( { args => dclone($args), %args } )->process_options;
85              
86 10 100       31 $self->error( "$Program: -P ignored\n" ) if $self->is_overwrite;
87              
88 10 50       18 unless ( () = $self->files ) {
89 0         0 $self->error( "$Program: missing argument\n" );
90 0         0 exit EX_FAILURE;
91             }
92              
93 10         13 my $errors = grep { $self->process_file( $_ ) } $self->files;
  13         22  
94 10 100       43 $self->exit( $errors ? EX_FAILURE : EX_SUCCESS );
95             }
96              
97             sub new {
98 32     32   37086 my( $class, $args ) = @_;
99 32         75 bless {
100             $class->defaults,
101             %$args
102             }, $class;
103             }
104              
105             sub defaults {
106 32     32   326 my %hash = (
107             args => [],
108             error_fh => \*STDERR,
109             output_fh => \*STDOUT,
110             );
111             }
112              
113 0     0   0 sub exit { my $self = shift; exit(shift) }
  0         0  
114              
115 30     30   6586 sub files { my $self = shift; @{ $self->{files} } }
  30         37  
  30         85  
116              
117 44     44   14836 sub is_force { my $self = shift; $self->{options}{f} }
  44         164  
118 25     25   43 sub is_interactive { my $self = shift; $self->{options}{i} }
  25         79  
119 20     20   30 sub is_overwrite { my $self = shift; $self->{options}{P} }
  20         64  
120 14     14   21 sub is_recursive { my $self = shift; $self->{options}{R} }
  14         62  
121 19     19   33 sub is_verbose { my $self = shift; $self->{options}{v} }
  19         61  
122              
123 10     10   4734 sub options { my $self = shift; $self->{options} }
  10         23  
124              
125             sub preprocess_options {
126 32     32   43 my( $self ) = @_;
127              
128 32         38 my @new_args = @{ $self->{args} };
  32         134  
129              
130 32         89 my %args = map { $new_args[$_], $_ } 0 .. $#new_args;
  86         218  
131              
132 32         55 my @rest;
133 32 100       79 if( exists $args{'--'} ) {
134 9         24 @rest = @new_args[ $args{'--'} .. $#new_args ];
135 9         23 @new_args = @new_args[0 .. ($args{'--'} - 1)];
136             }
137              
138             # Expand clustering
139             @new_args = map {
140 32 100       44 if( /-(.+)/ ) {
  64         203  
141 35         76 my $cluster = $1;
142 35         76 map { "-$_" } split //, $cluster;
  47         127  
143             }
144             else {
145 29         50 $_;
146             }
147             } @new_args;
148              
149             # this is rm particular processing: -f and -i turn off each
150             # other, and the last one wins. Figure out which one is last
151             # then filter out all earlier of the other.
152 32 100 100     104 if( exists $args{'-f'} && exists $args{'-i'} ) {
153 6         9 my $last;
154 6         11 foreach ( reverse @new_args ) {
155 6 50       20 next unless /\A-[fi]\z/;
156 6         8 $last = $_;
157 6         7 last;
158             }
159              
160             @new_args = map {
161 6         11 (
162 12 100 100     66 ( $last eq '-f' and $_ eq '-i') # f wins
163             ||
164             ( $last ne '-f' and $_ eq '-f' ) # i wins
165             ) ? () : $_;
166             } @new_args;
167             }
168              
169 32         56 $self->{original_args} = $self->{args};
170 32         86 $self->{args} = $self->{preprocessed_args} = [ @new_args, @rest ];
171              
172 32         83 return $self;
173             }
174              
175             sub process_options {
176 20     20   39 my( $self ) = @_;
177              
178 20         45 $self->preprocess_options;
179              
180 20         1755 require Getopt::Long;
181              
182 20         21763 my %opts;
183             my $ret = Getopt::Long::GetOptionsFromArray(
184             $self->{args},
185             'f' => \$opts{'f'},
186             'i' => \$opts{'i'},
187             'P' => \$opts{'P'},
188             'R' => \$opts{'r'},
189             'r' => \$opts{'R'},
190 20         104 'v' => \$opts{'v'},
191             );
192              
193 20 100       9785 $self->{options} = { map { defined $_ ? $_ : 0 } %opts };
  240         365  
194 20         55 $self->{files} = $self->{args};
195              
196 20         51 return $self;
197             }
198              
199             sub process_file {
200 13     13   18 my( $self, $filename ) = @_;
201              
202 13         22 my $method = do {
203 13 100       187 if( -d $filename ) {
204 4 100       16 if( ! $self->is_recursive ) {
205 2 100       13 $self->error( "$Program: '$filename': is a directory\n" ) unless $self->is_force;
206 2 100       3 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
207             }
208 2         11 'remove_directory';
209             }
210             else {
211 9         26 'remove_file';
212             }
213             };
214              
215 11         37 my $result = $self->$method( $filename );
216 11 100       21 return $self->is_force ? OP_SUCCEEDED : $result;
217             }
218              
219             sub remove_directory {
220 2     2   6 my( $self, $dirname ) = @_;
221              
222 2         4 my $dh;
223 2 50       58 unless( opendir( $dh, $dirname ) ) {
224 0 0       0 $self->error( "$Program: cannot open '$dirname': $!\n" ) unless $self->is_force;
225 0 0       0 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
226             }
227              
228 2         64 foreach my $file ( readdir($dh) ) {
229 6 100 100     27 next if $file eq '.' || $file eq '..';
230 2         16 my $path = catfile( $dirname, $file );
231              
232 2 50       26 my $method = -d $path ? 'remove_directory' : 'remove_file';
233 2         7 my $result = $self->$method($path);
234             }
235              
236 2         35 closedir $dh;
237              
238 2 50       103 unless( rmdir $dirname ) {
239 0 0       0 $self->error( "$Program: cannot remove directory '$dirname': $!\n" ) unless $self->is_force;
240 0 0       0 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
241             }
242              
243 2 50       9 $self->message( "$dirname\n" ) if $self->is_verbose;
244              
245 2         11 return OP_SUCCEEDED;
246             }
247              
248             sub remove_file {
249 11     11   19 my( $self, $filename ) = @_;
250              
251             # Answering no to skip a file is not an error
252 11 50 66     125 if( ! -w $filename && $self->is_interactive ) {
    50          
253 0         0 $self->message( "$filename: Read-only ? " );
254 0 0       0 return OP_SUCCEEDED if =~ /^[Nn]/;
255             }
256             elsif( $self->is_interactive ) {
257 0         0 $self->message( "$filename: ? " );
258 0 0       0 return OP_SUCCEEDED if =~ /^[Nn]/;
259             }
260              
261 11 100       18 chmod '0777', $filename if $self->is_force;
262              
263 11 100       370 unless( unlink $filename ) {
264 4 100       10 $self->error( "$Program: cannot remove '$filename': $!\n" ) unless $self->is_force;
265 4 100       8 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
266             }
267              
268 7 50       34 $self->message( "$filename\n" ) if $self->is_verbose;
269              
270 7         15 return OP_SUCCEEDED;
271             }
272              
273             sub usage {
274 0     0   0 require Pod::Usage;
275 0         0 Pod::Usage::pod2usage({
276             -exitval => EX_USAGE,
277             -verbose => 2,
278             });
279             }
280              
281 4     4   10 sub error_fh { my $self = shift; $self->{error_fh} }
  4         21  
282             sub error {
283 4     4   4 my $self = shift;
284 4 50       5 print { $self->error_fh || * STDERR } @_;
  4         8  
285             }
286              
287 0     0     sub output_fh { my $self = shift; $self->{output_fh} }
  0            
288             sub message {
289 0     0     my $self = shift;
290 0 0         print { $self->output_fh || *STDOUT } @_;
  0            
291             }
292              
293             __PACKAGE__;