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