#!/usr/pkg/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

package PerlPowerTools::rm;

use strict;

use File::Basename;
use File::Spec::Functions;
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 && -e $filename && ! -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__;
