File Coverage

bin/rm
Criterion Covered Total %
statement 128 148 86.4
branch 46 74 62.1
condition 14 15 93.3
subroutine 27 30 90.0
pod n/a
total 215 267 80.5


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 [-fiPrRv] 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 - show the name of each file after it is removed
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   612159 use strict;
  2         2  
  2         64  
60              
61 2     2   7 use File::Basename;
  2         2  
  2         202  
62 2     2   717 use File::Spec::Functions;
  2         1206  
  2         144  
63 2     2   9 use Storable qw(dclone);
  2         14  
  2         118  
64              
65 2     2   6 use constant EX_SUCCESS => 0;
  2         2  
  2         113  
66 2     2   7 use constant EX_FAILURE => 1;
  2         2  
  2         93  
67 2     2   7 use constant EX_USAGE => 2;
  2         0  
  2         60  
68 2     2   6 use constant OP_SUCCEEDED => 0;
  2         8  
  2         81  
69 2     2   7 use constant OP_FAILED => 1;
  2         1  
  2         1522  
70              
71             my $Program = basename($0);
72              
73             __PACKAGE__->run( args => \@ARGV ) unless caller;
74              
75             sub run {
76 10     10   98528 my $class = shift;
77 10         25 my %args = @_;
78              
79 10         14 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         984 my $self = $class->new( { args => dclone($args), %args } )->process_options;
85              
86 10 100       27 $self->error( "$Program: -P ignored\n" ) if $self->is_overwrite;
87              
88 10 50       15 unless ( () = $self->files ) {
89 0 0       0 exit(EX_SUCCESS) if $self->is_force;
90 0         0 $self->error( "$Program: missing argument\n" );
91 0         0 usage();
92             }
93              
94 10         31 my $errors = grep { $self->process_file( $_ ) } $self->files;
  13         18  
95 10 100       40 exit( $errors ? EX_FAILURE : EX_SUCCESS );
96             }
97              
98             sub new {
99 32     32   39323 my( $class, $args ) = @_;
100 32         74 bless {
101             $class->defaults,
102             %$args
103             }, $class;
104             }
105              
106             sub defaults {
107 32     32   293 my %hash = (
108             args => [],
109             error_fh => \*STDERR,
110             output_fh => \*STDOUT,
111             );
112             }
113              
114 30     30   5899 sub files { my $self = shift; @{ $self->{files} } }
  30         26  
  30         73  
115              
116 44     44   16726 sub is_force { my $self = shift; $self->{options}{f} }
  44         244  
117 21     21   28 sub is_interactive { my $self = shift; $self->{options}{i} }
  21         67  
118 20     20   31 sub is_overwrite { my $self = shift; $self->{options}{P} }
  20         63  
119 14 100   14   21 sub is_recursive { my $self = shift; $self->{options}{R} || $self->{options}{r} }
  14         97  
120 19     19   27 sub is_verbose { my $self = shift; $self->{options}{v} }
  19         81  
121              
122 10     10   4077 sub options { my $self = shift; $self->{options} }
  10         25  
123              
124             sub preprocess_options {
125 32     32   40 my( $self ) = @_;
126              
127 32         38 my @new_args = @{ $self->{args} };
  32         76  
128              
129 32         68 my %args = map { $new_args[$_], $_ } 0 .. $#new_args;
  86         167  
130              
131 32         42 my @rest;
132 32 100       75 if( exists $args{'--'} ) {
133 9         24 @rest = @new_args[ $args{'--'} .. $#new_args ];
134 9         21 @new_args = @new_args[0 .. ($args{'--'} - 1)];
135             }
136 32         47 foreach (@new_args) {
137 64 50       124 if (m/\A\-\-/) {
138 0         0 warn "unknown option: '$_'\n";
139 0         0 usage();
140             }
141             }
142              
143             # Expand clustering
144             @new_args = map {
145 32 100       41 if( /\A\-(.+)/ ) {
  64         147  
146 35         86 my $cluster = $1;
147 35         59 map { "-$_" } split //, $cluster;
  47         102  
148             }
149             else {
150 29         58 $_;
151             }
152             } @new_args;
153              
154             # this is rm particular processing: -f and -i turn off each
155             # other, and the last one wins. Figure out which one is last
156             # then filter out all earlier of the other.
157 32 100 100     89 if( exists $args{'-f'} && exists $args{'-i'} ) {
158 6         27 my $last;
159 6         10 foreach ( reverse @new_args ) {
160 6 50       18 next unless /\A-[fi]\z/;
161 6         10 $last = $_;
162 6         7 last;
163             }
164              
165             @new_args = map {
166 6         8 (
167 12 100 100     65 ( $last eq '-f' and $_ eq '-i') # f wins
168             ||
169             ( $last ne '-f' and $_ eq '-f' ) # i wins
170             ) ? () : $_;
171             } @new_args;
172             }
173              
174 32         65 $self->{original_args} = $self->{args};
175 32         79 $self->{args} = $self->{preprocessed_args} = [ @new_args, @rest ];
176              
177 32         62 return $self;
178             }
179              
180             sub process_options {
181 20     20   41 my( $self ) = @_;
182              
183 20         70 $self->preprocess_options;
184              
185 2     2   1196 use Getopt::Long qw(:config no_ignore_case);
  2         18790  
  2         7  
186              
187 20         20 my %opts;
188             my $ret = Getopt::Long::GetOptionsFromArray(
189             $self->{args},
190             'f' => \$opts{'f'},
191             'i' => \$opts{'i'},
192             'P' => \$opts{'P'},
193             'R' => \$opts{'R'}, # both of these are recursive
194             'r' => \$opts{'r'},
195 20         186 'v' => \$opts{'v'},
196             );
197 20 50       8740 usage() unless $ret;
198              
199 20 100       46 $self->{options} = { map { defined $_ ? $_ : 0 } %opts };
  240         290  
200 20         51 $self->{files} = $self->{args};
201              
202 20         44 return $self;
203             }
204              
205             sub process_file {
206 13     13   17 my( $self, $filename ) = @_;
207              
208 13         9 my $method = do {
209 13 100       202 if( -d $filename ) {
210 4 100       11 if( ! $self->is_recursive ) {
211 2 100       3 $self->error( "$Program: '$filename': is a directory\n" ) unless $self->is_force;
212 2 100       4 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
213             }
214 2         3 'remove_directory';
215             }
216             else {
217 9         14 'remove_file';
218             }
219             };
220              
221 11         30 my $result = $self->$method( $filename );
222 11 100       27 return $self->is_force ? OP_SUCCEEDED : $result;
223             }
224              
225             sub remove_directory {
226 2     2   4 my( $self, $dirname ) = @_;
227              
228 2         2 my $dh;
229 2 50       53 unless( opendir( $dh, $dirname ) ) {
230 0 0       0 $self->error( "$Program: cannot open '$dirname': $!\n" ) unless $self->is_force;
231 0 0       0 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
232             }
233              
234 2         44 foreach my $file ( readdir($dh) ) {
235 6 100 100     18 next if $file eq '.' || $file eq '..';
236 2         11 my $path = catfile( $dirname, $file );
237              
238 2 50       18 my $method = -d $path ? 'remove_directory' : 'remove_file';
239 2         5 my $result = $self->$method($path);
240             }
241              
242 2         19 closedir $dh;
243              
244 2 50       104 unless( rmdir $dirname ) {
245 0 0       0 $self->error( "$Program: cannot remove directory '$dirname': $!\n" ) unless $self->is_force;
246 0 0       0 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
247             }
248              
249 2 50       5 $self->message( "$dirname\n" ) if $self->is_verbose;
250              
251 2         7 return OP_SUCCEEDED;
252             }
253              
254             sub remove_file {
255 11     11   18 my( $self, $filename ) = @_;
256              
257             # Answering no to skip a file is not an error
258 11 50 100     15 if( $self->is_interactive ) {
    50 66        
259 0         0 $self->message( "$filename: ? " );
260 0 0       0 return OP_SUCCEEDED if =~ /^[Nn]/;
261             }
262             elsif( !$self->is_force && -e $filename && ! -w $filename ) {
263 0         0 $self->message( "$filename: Read-only ? " );
264 0 0       0 return OP_SUCCEEDED if =~ /^[Nn]/;
265             }
266              
267 11 100       590 unless( unlink $filename ) {
268 4 100       4 $self->error( "$Program: cannot remove '$filename': $!\n" ) unless $self->is_force;
269 4 100       5 return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
270             }
271              
272 7 50       25 $self->message( "$filename\n" ) if $self->is_verbose;
273              
274 7         13 return OP_SUCCEEDED;
275             }
276              
277             sub usage {
278 0     0   0 require Pod::Usage;
279 0         0 Pod::Usage::pod2usage({
280             -exitval => EX_USAGE,
281             -verbose => 1,
282             });
283             }
284              
285 4     4   3 sub error_fh { my $self = shift; $self->{error_fh} }
  4         17  
286             sub error {
287 4     4   4 my $self = shift;
288 4 50       3 print { $self->error_fh || *STDERR } @_;
  4         9  
289             }
290              
291 0     0     sub output_fh { my $self = shift; $self->{output_fh} }
  0            
292             sub message {
293 0     0     my $self = shift;
294 0 0         print { $self->output_fh || *STDOUT } @_;
  0            
295             }
296              
297             __PACKAGE__;