File Coverage

blib/lib/File/Replace.pm
Criterion Covered Total %
statement 192 220 87.7
branch 142 158 91.1
condition 57 66 89.3
subroutine 26 30 86.6
pod 7 13 53.8
total 424 487 88.0


line stmt bran cond sub pod time code
1             #!perl
2             package File::Replace;
3 5     5   571260 use warnings;
  5         18  
  5         142  
4 5     5   23 use strict;
  5         9  
  5         77  
5 5     5   19 use Carp;
  5         9  
  5         231  
6 5     5   25 use warnings::register;
  5         10  
  5         507  
7 5     5   29 use IO::Handle; # allow method calls on filehandles on older Perls
  5         9  
  5         233  
8 5     5   26 use File::Temp qw/tempfile/;
  5         15  
  5         198  
9 5     5   23 use File::Basename qw/fileparse/;
  5         8  
  5         275  
10 5     5   1533 use File::Spec::Functions qw/devnull/;
  5         2839  
  5         237  
11 5     5   1992 use File::Copy ();
  5         9822  
  5         127  
12 5     5   27 use Fcntl qw/S_IMODE/;
  5         10  
  5         189  
13 5     5   25 use Exporter ();
  5         6  
  5         267  
14             BEGIN {
15 5     5   2143 require Hash::Util;
16             # apparently this wasn't available until 0.06 / Perl 5.8.9
17             # since this is just for internal typo prevention,
18             # we can fake it when it's not available
19             # uncoverable branch false
20             # uncoverable condition right
21             # uncoverable condition false
22 5 50 33     11778 if ($] ge '5.010' || defined &Hash::Util::lock_ref_keys)
23 5         27 { Hash::Util->import('lock_ref_keys') }
24 0         0 else { *lock_ref_keys = sub {} } # uncoverable statement
25             }
26              
27             # For AUTHOR, COPYRIGHT, AND LICENSE see Replace.pod
28              
29             ## no critic (RequireArgUnpacking)
30              
31             our $VERSION = '0.18';
32              
33             our @EXPORT_OK = qw/ replace replace2 replace3 inplace /;
34             our @CARP_NOT = qw/ File::Replace::SingleHandle File::Replace::DualHandle File::Replace::Inplace /;
35              
36             sub import {
37 5     5   85 my @mine;
38 5         14 for my $i (reverse 1..$#_)
39 1 50       8 { unshift @mine, splice @_, $i, 1 if $_[$i]=~/^-i|^-D$/ }
40 5 50 33     25 if ( @mine and my @i = grep {/^-i/} @mine ) {
  0         0  
41 0 0       0 croak "$_[0]: can't specify more than one -i switch" if @i>1;
42             # the following double-check is currently just paranoia, so ignore it in code coverage:
43             # uncoverable branch true
44 0 0       0 my ($ext) = $i[0]=~/^-i(.*)$/ or croak "failed to parse '$i[0]'";
45 0         0 my $debug = grep {/^-D$/} @mine;
  0         0  
46 0         0 require File::Replace::Inplace;
47 0         0 $File::Replace::Inplace::GlobalInplace = File::Replace::Inplace->new(backup=>$ext, debug=>$debug); ## no critic (ProhibitPackageVars)
48             }
49 5         484 goto &Exporter::import;
50             }
51              
52             sub inplace {
53 0     0 1 0 require File::Replace::Inplace;
54 0         0 return File::Replace::Inplace->new(@_);
55             }
56              
57             our $DISABLE_CHMOD;
58              
59             my %NEW_KNOWN_OPTS = map {$_=>1} qw/ debug layers create chmod
60             perms autocancel autofinish in_fh backup /;
61             sub new { ## no critic (ProhibitExcessComplexity)
62 79     79 1 176270 my $class = shift;
63 79 100       360 @_ or croak "$class->new: not enough arguments";
64             # set up the object
65 78         119 my $filename = shift;
66 78 100       185 my $_layers = @_%2 ? shift : undef;
67 78         167 my %opts = @_;
68 78         185 for (keys %opts) { croak "$class->new: unknown option '$_'"
69 69 100       2625 unless $NEW_KNOWN_OPTS{$_} }
70             croak "$class->new: can't use autocancel and autofinish at once"
71 56 100 100     253 if $opts{autocancel} && $opts{autofinish};
72 55 100       112 unless (defined wantarray) { warnings::warnif("Useless use of $class->new in void context"); return }
  1         218  
  1         9  
73 54 100       100 if (defined $opts{create}) { # normalize 'create' values
74 9 100 100     59 if ( $opts{create} eq 'off' || $opts{create} eq 'no' )
    100 100        
75 3         8 { $opts{create} = 'off' }
76             elsif ( $opts{create} eq 'now' || $opts{create} eq 'later' )
77             { } # nothing needed
78 3         433 else { croak "bad value for 'create' option, must be one of off/no/later/now" }
79             }
80 45         80 else { $opts{create} = 'later' } # default
81             # create the object
82 51         204 my $self = bless { chmod=>!$DISABLE_CHMOD, %opts, is_open=>0 }, $class;
83 51 100 100     171 $self->{debug} = \*STDERR if $self->{debug} && !ref($self->{debug});
84 51 100       103 if (defined $_layers) {
85 6 100       118 exists $self->{layers} and croak "$class->new: layers specified twice";
86 5         11 $self->{layers} = $_layers }
87 50         271 lock_ref_keys $self, keys %NEW_KNOWN_OPTS, qw/ ifn ifh ofn ofh is_open setperms /;
88             # note: "perms" is the option the user explicitly sets and that options()
89             # needs to return, "setperms" is what finish() will actually set
90 50 100       4348 $self->{setperms} = $self->{perms} if defined $self->{perms};
91             # temporary output file
92 50         829 my ($basename,$path) = fileparse($filename);
93 50         216 ($self->{ofh}, $self->{ofn}) = tempfile( # croaks on error
94             ".${basename}_XXXXXXXXXX", DIR=>$path, SUFFIX=>'.tmp', UNLINK=>1 );
95 50 100       14922 binmode $self->{ofh}, $self->{layers} if defined $self->{layers};
96             # input file
97             # Possible To-Do for Later: A "noopen" option where the input file just isn't opened?
98 50 100       136 my $openmode = defined $self->{layers} ? '<'.$self->{layers} : '<';
99 50 100       1127 if ( defined $self->{in_fh} ) {
    100          
100 5 100       214 croak "in_fh appears to be closed" unless defined fileno($self->{in_fh});
101 4         17 $self->{ifh} = delete $self->{in_fh};
102             }
103             elsif ( not open $self->{ifh}, $openmode, $filename ) {
104             # No such file or directory:
105 25 100 100     203 if ( $!{ENOENT} && ($self->{create} eq 'now' || $self->{create} eq 'later') ) {
      66        
106 23 100       464 $self->{create} eq 'now' and $openmode = defined $self->{layers} ? '+>'.$self->{layers} : '+>';
    100          
107             # note we call &devnull() like this because otherwise it would
108             # be inlined and we want to be able to mock it for testing
109 23 100       661 if ( open $self->{ifh}, $openmode, $self->{create} eq 'now' ? $filename : &devnull() )
    50          
110 23 100       206 { $self->{setperms}=oct('666')&~umask unless defined $self->{setperms} }
111 0         0 else { $self->{ifh}=undef }
112 2         38 } else { $self->{ifh}=undef }
113             }
114 49 100       157 if ( !defined $self->{ifh} ) {
115 2         5 my $e=$!;
116 2         22 close $self->{ofh}; $self->{ofh} = undef;
  2         5  
117 2         58 unlink $self->{ofn}; $self->{ofn} = undef;
  2         9  
118 2         5 $!=$e; ## no critic (RequireLocalizedPunctuationVars)
119 2         315 croak "$class->new: failed to open '$filename': $!" }
120             else {
121 47 100       117 if (!defined $self->{setperms}) {
122 21 100       48 if ($self->{chmod}) {
123             # we're providing our own error, don't need the extra warning
124 5     5   3897 no warnings 'unopened'; ## no critic (ProhibitNoWarnings)
  5         9  
  5         8570  
125             my (undef,undef,$mode) = stat($self->{ifh})
126 19 100       280 or croak "stat failed: $!";
127 18         108 $self->{setperms} = S_IMODE($mode);
128             }
129 2         28 else { $self->{setperms}=0 }
130             }
131             }
132 46         83 $self->{ifn} = $filename;
133             # backup
134 46         71 my $debug_backup='';
135 46 100 100     190 if (defined($self->{backup}) && length($self->{backup})) {
136 4         12 my $bakfile = $filename . $self->{backup};
137 4 100       11 if ( $self->{backup}=~/\*/ ) {
138 1         4 ($bakfile = $self->{backup}) =~ s/\*/$basename/;
139 1         3 $bakfile = $path.$bakfile;
140             }
141 4 100       254 croak "backup failed: file '$bakfile' exists" if -e $bakfile;
142             # Possible To-Do for Later: Maybe a backup_link option that uses hard links instead of copy?
143 3 100       17 File::Copy::syscopy($filename, $bakfile)
144             or croak "backup failed: couldn't copy '$filename' to '$bakfile': $!";
145 2         528 $debug_backup = ', backup to \''.$bakfile."'";
146             }
147             # finish init
148 44         73 $self->{is_open} = 1;
149             $self->_debug("$class->new: input '", $self->{ifn},
150             "', output '", $self->{ofn}, "', layers ",
151 44 100       235 (defined $self->{layers} ? "'".$self->{layers}."'" : 'undef'),
152             $debug_backup, "\n");
153 44         228 return $self;
154             }
155              
156             sub replace3 {
157 4 100   4 1 11356 unless (defined wantarray) { warnings::warnif("Useless use of "
  1         151  
158 1         8 .__PACKAGE__."::replace3 in void context"); return }
159 3         30 my $repl = __PACKAGE__->new(@_);
160 3         10 return ($repl->in_fh, $repl->out_fh, $repl);
161             }
162              
163             sub replace2 {
164 0     0 1 0 require File::Replace::SingleHandle;
165 0 0       0 unless (defined wantarray) { warnings::warnif("Useless use of "
  0         0  
166 0         0 .__PACKAGE__."::replace2 in void context"); return }
167 0         0 my $repl = __PACKAGE__->new(@_);
168 0 0       0 if (wantarray) {
169             return (
170 0         0 File::Replace::SingleHandle->new($repl, 'in'),
171             File::Replace::SingleHandle->new($repl, 'out') );
172             }
173             else {
174 0         0 return File::Replace::SingleHandle->new($repl, 'onlyout');
175             }
176             }
177              
178             sub replace {
179 0     0 1 0 require File::Replace::DualHandle;
180 0 0       0 unless (defined wantarray) { warnings::warnif("Useless use of "
  0         0  
181 0         0 .__PACKAGE__."::replace in void context"); return }
182 0         0 my $repl = __PACKAGE__->new(@_);
183 0         0 return File::Replace::DualHandle->new($repl);
184             }
185              
186 0     0 0 0 sub is_open { return !!shift->{is_open} }
187 1     1 0 690 sub filename { return shift->{ifn} }
188 18     18 1 1888 sub in_fh { return shift->{ifh} }
189 20     20 0 1671 sub out_fh { return shift->{ofh} }
190              
191             sub options {
192 3     3 0 14 my $self = shift;
193 3         5 my %opts;
194 3         10 for my $o (keys %NEW_KNOWN_OPTS)
195 27 100       45 { exists $self->{$o} and $opts{$o} = $self->{$o} }
196 3 100       31 return wantarray ? %opts : \%opts;
197             }
198              
199             our $COPY_DEFAULT_BUFSIZE = 4096;
200             my %COPY_KNOWN_OPTS = map {$_=>1} qw/ count bufsize less /;
201             sub copy { ## no critic (ProhibitExcessComplexity)
202 23     23 1 6798 my $self = shift;
203 23 100       138 croak ref($self)."->copy: already closed" unless $self->{is_open};
204 22 100       53 my $_count = @_%2 ? shift : undef;
205 22         43 my %opts = @_;
206 22 100       40 if (defined $_count) {
207 14 100       109 exists $opts{count} and croak ref($self)."->copy: count specified twice";
208 13         19 $opts{count} = $_count }
209 21         43 for (keys %opts) { croak ref($self)."->copy: unknown option '$_'"
210 32 100       166 unless $COPY_KNOWN_OPTS{$_} }
211 20 100       96 $opts{bufsize} = $COPY_DEFAULT_BUFSIZE unless defined $opts{bufsize};
212 20 100 100     363 croak ref($self)."->copy: bad count" unless $opts{count} && $opts{count}=~/\A\d+\z/;
213 18 100 100     233 croak ref($self)."->copy: bad bufsize" unless $opts{bufsize} && $opts{bufsize}=~/\A\d+\z/;
214             croak ref($self)."->copy: bad less option" if defined $opts{less}
215 16 100 100     137 && $opts{less}!~/\A(?:ok|ignore)\z/;
216 15         22 my $remain = $opts{count};
217 15   100     121 while ( $remain>0 && !eof($self->{ifh}) ) {
218             my $in = read $self->{ifh}, my $buf,
219 15 100       82 $remain > $opts{bufsize} ? $opts{bufsize} : $remain;
220 15 100       131 defined $in or croak ref($self)."->copy: read failed: $!";
221 14 100       16 print {$self->{ofh}} $buf or croak ref($self)."->copy: write failed: $!";
  14         63  
222 13         70 $remain -= $in;
223             }
224             warnings::warnif(ref($self)."->copy: read $remain less characters than requested")
225 13 100 100     529 if $remain && !$opts{less};
226 12         89 return $opts{count}-$remain;
227             }
228              
229             sub finish {
230 32     32 0 7067 my $self = shift;
231 32 100       234 @_ and warnings::warnif(ref($self)."->finish: too many arguments");
232 32 100       126 if (!$self->{is_open}) {
233 1         138 warnings::warnif(ref($self)."->finish: already closed");
234 1         28 return }
235 31         48 my ($ifn,$ifh,$ofn,$ofh) = @{$self}{qw/ifn ifh ofn ofh/};
  31         88  
236 31         80 @{$self}{qw/ifh ofh ofn ifn is_open/} = (undef) x 5;
  31         61  
237             # Note we're being conservative here because if any of the steps fail,
238             # then it's fairly safe to assume the following steps will fail too.
239 31         35 my $fail;
240 31 100 66     3577 if ( defined(fileno($ifh)) && !close($ifh) ) ## no critic (ProhibitCascadingIfElse)
    100 66        
    50 66        
    50          
241 1         34 { $fail = "couldn't close input handle" }
242             elsif ( defined(fileno($ofh)) && !close($ofh) )
243 1         24 { $fail = "couldn't close output handle" }
244             elsif ( $self->{chmod} && !chmod($self->{setperms}, $ofn) )
245 0         0 { $fail = "couldn't chmod '$ofn'" }
246             elsif ( not rename($ofn, $ifn) )
247 0         0 { $fail = "couldn't rename '$ofn' to '$ifn'" }
248 31 100       237 if ( defined $fail ) {
249 2         10 my $e=$!; unlink($ofn); $!=$e; ## no critic (RequireLocalizedPunctuationVars)
  2         54  
  2         10  
250 2         255 croak ref($self)."->finish: $fail: $!";
251             }
252             $self->_debug(ref($self),"->finish: renamed '$ofn' to '$ifn', perms ",
253 29         299 sprintf('%05o',$self->{setperms}), "\n");
254 29         138 return 1;
255             }
256              
257             sub _cancel {
258 64     64   153 my $self = shift;
259 64         85 my $from = shift;
260 64 100       139 if ($from eq 'destroy')
    100          
261             { $self->{is_open} and warnings::warnif(ref($self)
262 51 100       523 .": unclosed file '".$self->{ifn}."' not replaced!") }
263             elsif ($from eq 'cancel')
264 11 100       456 { $self->{is_open} or warnings::warnif(ref($self)."->cancel: already closed") }
265 64 100 100     354 if (!($from eq 'destroy' && !$self->{is_open}))
266             { $self->_debug(ref($self), "->cancel: not replacing input file ",
267             (defined $self->{ifn} ? "'$self->{ifn}'" : "(unknown)"),
268 15 100       89 (defined $self->{ofn} ? ", will attempt to unlink '$self->{ofn}'" : ""), "\n") }
    100          
269 64         110 my ($ifh,$ofh,$ofn) = @{$self}{qw/ifh ofh ofn/};
  64         148  
270 64         146 @{$self}{qw/ifh ofh ofn ifn is_open/} = (undef) x 5;
  64         115  
271 64         82 my $success = 1;
272 64 100 100     297 defined($ifh) and defined(fileno($ifh)) and close($ifh) or $success=0;
      100        
273 64 100 66     404 defined($ofh) and defined(fileno($ofh)) and close($ofh) or $success=0;
      100        
274 64 100       725 defined($ofn) and unlink($ofn);
275 64 100       126 if ($success) { return 1 } else { return }
  13         80  
  51         93  
276             }
277              
278 11     11 0 1884 sub cancel { return shift->_cancel('cancel') }
279              
280             sub DESTROY {
281 51     51   17242 my $self = shift;
282 51 100       122 if ($self->{is_open}) {
283 7 100       22 if ($self->{autocancel}) { $self->cancel }
  3 100       35  
284 2         7 elsif ($self->{autofinish}) { $self->finish }
285             }
286 51         129 $self->_cancel('destroy');
287 51         373 return;
288             }
289              
290             sub _debug { ## no critic (RequireArgUnpacking)
291 89     89   140 my $self = shift;
292 89 100       209 return 1 unless $self->{debug};
293 10 100       144 confess "not enough arguments to _debug" unless @_;
294 9         29 local ($",$,,$\) = (' ');
295 9         9 return print {$self->{debug}} @_;
  9         227  
296             }
297              
298             1;