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   998 use strict;
  2         3  
  2         64  
60              
61 2     2   12 use File::Basename;
  2         2  
  2         129  
62 2     2   930 use File::Spec::Functions;
  2         1631  
  2         144  
63 2     2   1282 use Storable qw(dclone);
  2         6620  
  2         139  
64              
65 2     2   18 use constant EX_SUCCESS => 0;
  2         2  
  2         172  
66 2     2   11 use constant EX_FAILURE => 1;
  2         3  
  2         109  
67 2     2   11 use constant EX_USAGE => 2;
  2         3  
  2         84  
68 2     2   10 use constant OP_SUCCEEDED => 0;
  2         5  
  2         87  
69 2     2   10 use constant OP_FAILED => 1;
  2         2  
  2         3562  
70              
71             my $Program = basename($0);
72              
73             __PACKAGE__->run( args => \@ARGV ) unless caller;
74              
75             sub run {
76 10     10   88013 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         543 my $self = $class->new( { args => dclone($args), %args } )->process_options;
85              
86 10 100       33 $self->error( "$Program: -P ignored\n" ) if $self->is_overwrite;
87              
88 10 50       17 unless ( () = $self->files ) {
89 0         0 $self->error( "$Program: missing argument\n" );
90 0         0 exit EX_FAILURE;
91             }
92              
93 10         14 my $errors = grep { $self->process_file( $_ ) } $self->files;
  13         21  
94 10 100       43 $self->exit( $errors ? EX_FAILURE : EX_SUCCESS );
95             }
96              
97             sub new {
98 32     32   33898 my( $class, $args ) = @_;
99 32         70 bless {
100             $class->defaults,
101             %$args
102             }, $class;
103             }
104              
105             sub defaults {
106 32     32   303 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   6284 sub files { my $self = shift; @{ $self->{files} } }
  30         34  
  30         83  
116              
117 44     44   13774 sub is_force { my $self = shift; $self->{options}{f} }
  44         163  
118 25     25   42 sub is_interactive { my $self = shift; $self->{options}{i} }
  25         79  
119 20     20   34 sub is_overwrite { my $self = shift; $self->{options}{P} }
  20         60  
120 14     14   21 sub is_recursive { my $self = shift; $self->{options}{R} }
  14         52  
121 19     19   33 sub is_verbose { my $self = shift; $self->{options}{v} }
  19         88  
122              
123 10     10   4244 sub options { my $self = shift; $self->{options} }
  10         23  
124              
125             sub preprocess_options {
126 32     32   50 my( $self ) = @_;
127              
128 32         34 my @new_args = @{ $self->{args} };
  32         135  
129              
130 32         84 my %args = map { $new_args[$_], $_ } 0 .. $#new_args;
  86         212  
131              
132 32         49 my @rest;
133 32 100       76 if( exists $args{'--'} ) {
134 9         26 @rest = @new_args[ $args{'--'} .. $#new_args ];
135 9         20 @new_args = @new_args[0 .. ($args{'--'} - 1)];
136             }
137              
138             # Expand clustering
139             @new_args = map {
140 32 100       39 if( /-(.+)/ ) {
  64         194  
141 35         66 my $cluster = $1;
142 35         71 map { "-$_" } split //, $cluster;
  47         122  
143             }
144             else {
145 29         53 $_;
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     100 if( exists $args{'-f'} && exists $args{'-i'} ) {
153 6         7 my $last;
154 6         9 foreach ( reverse @new_args ) {
155 6 50       22 next unless /\A-[fi]\z/;
156 6         8 $last = $_;
157 6         8 last;
158             }
159              
160             @new_args = map {
161 6         10 (
162 12 100 100     65 ( $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         49 $self->{original_args} = $self->{args};
170 32         67 $self->{args} = $self->{preprocessed_args} = [ @new_args, @rest ];
171              
172 32         77 return $self;
173             }
174              
175             sub process_options {
176 20     20   66 my( $self ) = @_;
177              
178 20         59 $self->preprocess_options;
179              
180 20         1648 require Getopt::Long;
181              
182 20         21500 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         93 'v' => \$opts{'v'},
191             );
192              
193 20 100       9620 $self->{options} = { map { defined $_ ? $_ : 0 } %opts };
  240         360  
194 20         50 $self->{files} = $self->{args};
195              
196 20         47 return $self;
197             }
198              
199             sub process_file {
200 13     13   23 my( $self, $filename ) = @_;
201              
202 13         12 my $method = do {
203 13 100       188 if( -d $filename ) {
204 4 100       16 if( ! $self->is_recursive ) {
205 2 100       5 $self->error( "$Program: '$filename': is a directory\n" ) unless $self->is_force;
206 2 100       7 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
207             }
208 2         5 'remove_directory';
209             }
210             else {
211 9         23 'remove_file';
212             }
213             };
214              
215 11         38 my $result = $self->$method( $filename );
216 11 100       18 return $self->is_force ? OP_SUCCEEDED : $result;
217             }
218              
219             sub remove_directory {
220 2     2   5 my( $self, $dirname ) = @_;
221              
222 2         3 my $dh;
223 2 50       56 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         47 foreach my $file ( readdir($dh) ) {
229 6 100 100     27 next if $file eq '.' || $file eq '..';
230 2         13 my $path = catfile( $dirname, $file );
231              
232 2 50       25 my $method = -d $path ? 'remove_directory' : 'remove_file';
233 2         8 my $result = $self->$method($path);
234             }
235              
236 2         27 closedir $dh;
237              
238 2 50       90 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       8 $self->message( "$dirname\n" ) if $self->is_verbose;
244              
245 2         9 return OP_SUCCEEDED;
246             }
247              
248             sub remove_file {
249 11     11   18 my( $self, $filename ) = @_;
250              
251             # Answering no to skip a file is not an error
252 11 50 66     120 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       19 chmod '0777', $filename if $self->is_force;
262              
263 11 100       358 unless( unlink $filename ) {
264 4 100       13 $self->error( "$Program: cannot remove '$filename': $!\n" ) unless $self->is_force;
265 4 100       6 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
266             }
267              
268 7 50       30 $self->message( "$filename\n" ) if $self->is_verbose;
269              
270 7         16 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   4 sub error_fh { my $self = shift; $self->{error_fh} }
  4         27  
282             sub error {
283 4     4   9 my $self = shift;
284 4 50       5 print { $self->error_fh || * STDERR } @_;
  4         5  
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__;