The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package clobber;
use Carp;
use Fcntl;
use strict; no strict 'refs';
use vars '$VERSION'; $VERSION = 0.04;
eval "require Term::ReadKey";

sub unimport { #no strict 'refs';
  *{'CORE::GLOBAL::open'}    = \&OPEN    unless exists($^H{clobber});
  *{'CORE::GLOBAL::sysopen'} = \&SYSOPEN unless exists($^H{clobber});
  $^H{clobber} = $ENV{'clobber.pm'} || 0;
}

sub import {
  $^H{clobber} = 1;
}


sub OPEN(*;$@){
  my($handle, $mode, $file) = @_;
  my($testmode, $pipein) = $mode;

  if( scalar(@_) == 1 ){ #no strict 'refs';
    $mode = ${caller(1).'::'.$handle};
  }

  if( scalar(@_) == 2 ){
    #Convert 2-arg to 3-arg...
    #Initially tried to simply pass @_ through to CORE::open,
    #but it's prototype didn't like that

    #put into sub for /x, and easier "testing"?
    if( $mode =~ /^(\+?(?:>{1,2}|<)|(?:>&=?|<&=?|\|))?\s*(.+)\s*(\|)?$/ ){
      ($testmode, $file, $pipein) = ($1, $2, $3);
    }
    else{
      croak "Failed to parse EXPR of 2-arg open: $_[1]";
    }

    $testmode = $1 eq '|' ? '|-' : $1;
    unless( defined $testmode ){
      $testmode = $pipein ? '-|' : '>';
    }
  }
  elsif( scalar(@_) > 2 ){
    ($testmode, $file) = @_[1,2];
  }

  prompt($file) if -e $file && $testmode =~ /\+[<>](?!>)|^>(?!&|>)/;

  splice(@_, 0, 3);

  #no strict 'refs';
  CORE::open(*{caller(0) . '::' . $handle}, $testmode, $file, @_);
}

sub SYSOPEN(*$$;$){
  my($handle, $file, $mode, $perms) = @_;

  #We don't use O_EXCL because sysopen's failure is not trappable
  prompt($file) if -e $file && $mode&(O_WRONLY|O_RDWR|O_TRUNC);

  #no strict 'refs';
  CORE::sysopen(*{caller(0) . '::' . $handle}, $file, $mode, $perms||0666);
}

sub prompt{
  my $clobber = 0;

  return if (caller 1)[10]->{clobber};

  if( -t STDIN && exists($INC{'Term/ReadKey.pm'}) ){

    select(STDERR); local $|=1;
    print STDERR "Allow modification of '$_[0]'? [yN] ";

    Term::ReadKey::ReadMode('cbreak'); $clobber = Term::ReadKey::ReadKey(0);

    Term::ReadKey::ReadMode('restore'); print STDERR "\n";

    $clobber =~ y/yY/1/; $clobber =~ y/1/0/c;
  }

  croak "$_[0]: File exists" unless $clobber;
}


1;
__END__

=pod

=head1 NAME

clobber - pragma to optionally prevent over-writing files

=head1 SYNOPSIS

  no clobber;

  #Fails if /tmp/xyzzy exists
  open(HNDL, '>/tmp/xyzzy');

  {
    use clobber;

    #It's clobberin' time
    open(HNDL, '>/tmp/xyzzy');
  }

=head1 DESCRIPTION

Do you occasionally get C<+E<gt>> and C<+E<lt>> mixed up, or accidentally
leave off an E<gt> in the mode of an C<open>? Want to run some relatively
trustworthy code--such as some spaghetti monster you created in the days
of yore--but can't be bothered to check it's semantics? Or perhaps you'd
like to add a level of protection to operations on user-supplied files
without coding the logic yourself.

Yes? Then this pragma could help you from blowing away valuable data.

Like the I<noclobber> variable of some shells, this module will prevent
the use of open modes which truncate if a file already exists. This behavior
can be controlled at the block level, as demonstrated in the L</SYNOPSIS>.

=head1 DIAGNOSTICS

The pragma may throw the following exceptions:

=over

=item %s: File exists

We saved data!

=item Failed to parse EXPR of 2-arg open: %s

The module could not figure out what mode was used,
and decided to bail for safety.

This shouldn't happen.

=back

=head1 ENVIRONMENT

You may disable clobber protection at compile-time by setting the environment
variable I<clobber.pm> to 1. This allows you to include F<clobber.pm> in
I<PERL5OPT> as B<-M-clobber> for general protection, but override it as needed
for programs invoked via a pipeline.

=head1 TODO

=over

=item TESTS!

I've done some basic-testing with 2- and 3-arg forms of read/write/append,
but more thorough testing of mode-parsing and/or invocation needs to be done.

Interactive ask to run the more complex tests, with timeout to skip them.

=item wrap other data-damaging functions such as unlink and truncate?

as optional "imports"

=back

=head1 AUTHOR

Jerrad Pierce E<lt>JPIERCE circle-a CPAN full-stop ORGE<gt>

=head1 LICENSE

=over

=item * Thou shalt not claim ownership of unmodified materials.

=item * Thou shalt not claim whole ownership of modified materials.

=item * Thou shalt grant the indemnity of the provider of materials.

=item * Thou shalt use and dispense freely without other restrictions.

=back

Or, if you prefer:

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.0 or,
at your option, any later version of Perl 5 you may have available.

=cut