The Perl Toolchain Summit 2025 Needs You: You can help 🙏 Learn more

#!/usr/bin/perl
=encoding utf8
=begin metadata
Name: rm
Description: remove directory entries
Author: brian d foy, bdfoy@cpan.org
License: artistic2
=end metadata
=cut
=head1 NAME
rm - remove directory entries
=head1 SYNOPSIS
rm [-fiPrRv] file ...
=head1 DESCRIPTION
=head1 OPTIONS
=over 4
=item * -f - do not prompt the user for each file, and do not consider it an error if a file cannot be removed
=item * -i - prompt the user for each file.
=item * -P - a no-op, for compatibility. So implementations would overwrite files with random data
=item * -r - same as -R
=item * -R - remove directories recursively
=item * -v - show the name of each file after it is removed
=back
=head1 AUTHOR
Copyright (c) brian d foy, bdfoy@cpan.org
The original version of this program was written by Steve Kemp,
steve@steve.org.uk, but almost none of that remains.
=head1 LICENCE
This program is licensed under the Artistic License 2.0.
=cut
use strict;
use Storable qw(dclone);
use constant EX_SUCCESS => 0;
use constant EX_FAILURE => 1;
use constant EX_USAGE => 2;
use constant OP_SUCCEEDED => 0;
use constant OP_FAILED => 1;
my $Program = basename($0);
__PACKAGE__->run( args => \@ARGV ) unless caller;
sub run {
my $class = shift;
my %args = @_;
my $args = delete $args{args};
# This looks funny because the other args are filehandles, which
# we can't dupe. We want to play with the command-line args such
# that we don't mess up anything that called us.
my $self = $class->new( { args => dclone($args), %args } )->process_options;
$self->error( "$Program: -P ignored\n" ) if $self->is_overwrite;
unless ( () = $self->files ) {
exit(EX_SUCCESS) if $self->is_force;
$self->error( "$Program: missing argument\n" );
usage();
}
my $errors = grep { $self->process_file( $_ ) } $self->files;
exit( $errors ? EX_FAILURE : EX_SUCCESS );
}
sub new {
my( $class, $args ) = @_;
bless {
$class->defaults,
%$args
}, $class;
}
sub defaults {
my %hash = (
args => [],
error_fh => \*STDERR,
output_fh => \*STDOUT,
);
}
sub files { my $self = shift; @{ $self->{files} } }
sub is_force { my $self = shift; $self->{options}{f} }
sub is_interactive { my $self = shift; $self->{options}{i} }
sub is_overwrite { my $self = shift; $self->{options}{P} }
sub is_recursive { my $self = shift; $self->{options}{R} || $self->{options}{r} }
sub is_verbose { my $self = shift; $self->{options}{v} }
sub options { my $self = shift; $self->{options} }
sub preprocess_options {
my( $self ) = @_;
my @new_args = @{ $self->{args} };
my %args = map { $new_args[$_], $_ } 0 .. $#new_args;
my @rest;
if( exists $args{'--'} ) {
@rest = @new_args[ $args{'--'} .. $#new_args ];
@new_args = @new_args[0 .. ($args{'--'} - 1)];
}
foreach (@new_args) {
if (m/\A\-\-/) {
warn "unknown option: '$_'\n";
usage();
}
}
# Expand clustering
@new_args = map {
if( /\A\-(.+)/ ) {
my $cluster = $1;
map { "-$_" } split //, $cluster;
}
else {
$_;
}
} @new_args;
# this is rm particular processing: -f and -i turn off each
# other, and the last one wins. Figure out which one is last
# then filter out all earlier of the other.
if( exists $args{'-f'} && exists $args{'-i'} ) {
my $last;
foreach ( reverse @new_args ) {
next unless /\A-[fi]\z/;
$last = $_;
last;
}
@new_args = map {
(
( $last eq '-f' and $_ eq '-i') # f wins
||
( $last ne '-f' and $_ eq '-f' ) # i wins
) ? () : $_;
} @new_args;
}
$self->{original_args} = $self->{args};
$self->{args} = $self->{preprocessed_args} = [ @new_args, @rest ];
return $self;
}
sub process_options {
my( $self ) = @_;
$self->preprocess_options;
use Getopt::Long qw(:config no_ignore_case);
my %opts;
my $ret = Getopt::Long::GetOptionsFromArray(
$self->{args},
'f' => \$opts{'f'},
'i' => \$opts{'i'},
'P' => \$opts{'P'},
'R' => \$opts{'R'}, # both of these are recursive
'r' => \$opts{'r'},
'v' => \$opts{'v'},
);
usage() unless $ret;
$self->{options} = { map { defined $_ ? $_ : 0 } %opts };
$self->{files} = $self->{args};
return $self;
}
sub process_file {
my( $self, $filename ) = @_;
my $method = do {
if( -d $filename ) {
if( ! $self->is_recursive ) {
$self->error( "$Program: '$filename': is a directory\n" ) unless $self->is_force;
return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
}
'remove_directory';
}
else {
'remove_file';
}
};
my $result = $self->$method( $filename );
return $self->is_force ? OP_SUCCEEDED : $result;
}
sub remove_directory {
my( $self, $dirname ) = @_;
my $dh;
unless( opendir( $dh, $dirname ) ) {
$self->error( "$Program: cannot open '$dirname': $!\n" ) unless $self->is_force;
return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
}
foreach my $file ( readdir($dh) ) {
next if $file eq '.' || $file eq '..';
my $path = catfile( $dirname, $file );
my $method = -d $path ? 'remove_directory' : 'remove_file';
my $result = $self->$method($path);
}
closedir $dh;
unless( rmdir $dirname ) {
$self->error( "$Program: cannot remove directory '$dirname': $!\n" ) unless $self->is_force;
return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
}
$self->message( "$dirname\n" ) if $self->is_verbose;
return OP_SUCCEEDED;
}
sub remove_file {
my( $self, $filename ) = @_;
# Answering no to skip a file is not an error
if( $self->is_interactive ) {
$self->message( "$filename: ? " );
return OP_SUCCEEDED if <STDIN> =~ /^[Nn]/;
}
elsif( !$self->is_force && ! -w $filename ) {
$self->message( "$filename: Read-only ? " );
return OP_SUCCEEDED if <STDIN> =~ /^[Nn]/;
}
unless( unlink $filename ) {
$self->error( "$Program: cannot remove '$filename': $!\n" ) unless $self->is_force;
return $self->is_force ? OP_SUCCEEDED : OP_FAILED;
}
$self->message( "$filename\n" ) if $self->is_verbose;
return OP_SUCCEEDED;
}
sub usage {
require Pod::Usage;
Pod::Usage::pod2usage({
-exitval => EX_USAGE,
-verbose => 1,
});
}
sub error_fh { my $self = shift; $self->{error_fh} }
sub error {
my $self = shift;
print { $self->error_fh || *STDERR } @_;
}
sub output_fh { my $self = shift; $self->{output_fh} }
sub message {
my $self = shift;
print { $self->output_fh || *STDOUT } @_;
}
__PACKAGE__;