package File::Inplace; use strict; use Carp qw/carp croak/; use File::Basename qw/dirname/; use File::Temp qw/tempfile/; use File::Copy; use IO::File; use IO::Handle; our $VERSION = '0.20'; my @allowed_options = qw/chomp regex separator suffix file/; my %allowed_options = map { $_ => 1 } @allowed_options; sub new { my $class = shift; my %params = @_; for my $opt (keys %params) { croak "Invalid constructor option '$opt'" unless exists $allowed_options{$opt}; } croak "Required parameter 'file' not specified in constructor" unless exists $params{file}; my $self = bless \%params, $class; $params{chomp} = 1 unless exists $params{chomp}; $params{regex} = $params{regex} || $params{separator} || qr/\s+/; $params{separator} ||= ' '; if ($self->{suffix}) { $self->{backup_name} = $self->{file} . $self->{suffix}; copy($self->{file} => $self->{backup_name}) or croak "error creating backup: $!"; } $self->_open_input_file; $self->_open_output_file; $self->{current_line} = undef; return $self; } sub has_lines { my $self = shift; return 1 if not $self->{infh}->eof(); return 0; } sub next_line { my $self = shift; $self->_write_current_line; $self->{current_line} = $self->_read_next_line(); if (wantarray) { if (defined $self->{current_line}) { return ($self->{current_line}); } else { return (); } } return $self->{current_line}; } sub next_line_split { my $self = shift; my $line = $self->next_line; return split $self->{regex}, $line; } sub all_lines { my $self = shift; croak "cannot use all_lines after any lines have been read" if defined $self->{current_line}; my @ret; while (1) { my $line = $self->_read_next_line; last unless defined $line; push @ret, $line; } return @ret; } sub replace_line { my $self = shift; if (@_ == 1) { $self->{current_line} = shift; } else { $self->{current_line} = join($self->{separator}, @_); } } sub replace_lines { my $self = shift; my @lines = @_; my $fh = $self->{outfh}; for my $line (@lines) { $fh->print($line); if ($self->{chomp}) { $fh->print($/); } } } sub _open_input_file { my $self = shift; $self->{infh} = new IO::File("<$self->{file}"); croak "open $self->{file}: $!" if not $self->{infh}; } sub _open_output_file { my $self = shift; my $dir = dirname $self->{file}; my ($tmpfh, $tmpname) = tempfile(DIR => $dir); $self->{outfh} = bless $tmpfh, "IO::Handle"; $self->{tmpfile} = $tmpname; } sub _write_current_line { my $self = shift; my $fh = $self->{outfh}; if (defined $self->{current_line}) { $fh->print($self->{current_line}); if ($self->{chomp}) { $fh->print($/); } } } sub _read_next_line { my $self = shift; my $fh = $self->{infh}; return undef unless $fh; my $line = $fh->getline; if (not defined $line) { $fh->close; delete $self->{infh}; } if (defined $line and $self->{chomp}) { chomp $line; } return $line; } sub commit { my $self = shift; $self->_write_current_line; rename $self->{tmpfile} => $self->{file} or croak "Can't rename $self->{tmpname} => $self->{file}: $!"; $self->_close_all(); } sub commit_to_backup { my $self = shift; $self->_write_current_line; croak "cannot commit_to_backup if no backup file is in use" unless $self->{backup_name}; rename $self->{tmpfile} => $self->{backup_name} or croak "Can't rename $self->{tmpname} => $self->{backup_name}: $!"; $self->_close_all(); } sub rollback { my $self = shift; $self->_close_all(); unlink $self->{tmpfile}; } sub DESTROY { my $self = shift; $self->_close_all(); unlink $self->{tmpfile}; } sub _close_all { my $self = shift; for my $handle (qw/infh outfh/) { $self->{$handle}->close() if $self->{$handle}; } } 1; __END__ =head1 NAME File::Inplace - Perl module for in-place editing of files =head1 SYNOPSIS use File::Inplace; my $editor = new File::Inplace(file => "file.txt"); while (my ($line) = $editor->next_line) { $editor->replace_line(reverse $line); } $editor->commit; =head1 DESCRIPTION File::Inplace is a perl module intended to ease the common task of editing a file in-place. Inspired by variations of perl's -i option, this module is intended for somewhat more structured and reusable editing than command line perl typically allows. File::Inplace endeavors to guarantee file integrity; that is, either all of the changes made will be saved to the file, or none will. It also offers functionality such as backup creation, automatic field splitting per-line, automatic chomping/unchomping, and aborting edits partially through without affecting the original file. =head1 CONSTRUCTOR File::Inplace offers one constructor that accepts a number of parameters, one of which is required. =over 4 =item File::Inplace->new(file => "filename", ...) =over 4 =item file The one required parameter. This is the name of the file to edit. =item suffix The suffix for backup files. If not specified, no backups are made. =item chomp If set to zero, then automatic chomping will not be performed. Newlines (actually, the contents of $/) will remain in strings returned from C. Additionally, the contents of $/ will not be appended when replacing lines. =item regex If specified, then each line will be split by this parameter when using C method. If unspecified, then this defaults to \s+. =item separator The default character used to join each line when replace_line is invoked with a list instead of a single value. Defaults to a single space. =back =head1 INSTANCE METHODS =item $editor->next_line () In scalar context, it returns the next line of the input file, or undef if there is no line. In an array context, it returns a single value of the line, or an empty list if there is no line. =item $editor->replace_line (value) Replaces the current line in the output file with the specified value. If passed a list, then each valie is joined by the C specified at construction time. =item $editor->next_line_split () Line C, except splits based on the C specified in the constructor. =item $editor->has_lines () Returns true if the file contains any further lines. =item $editor->all_lines () Returns an array of all lines in the file being edited. =item $editor->replace_all_lines (@lines) Replaces B remaining lines in the file with the specified @lines. =item $editor->commit () Completes the edit operation and saves the changes to the edited file. =item $editor->rollback () Aborts the edit process. =item $editor->commit_to_backup () Saves edits to the backup file instead of the original file. =back =head1 AUTHOR Chip Turner, Echipt@cpan.orgE =head1 COPYRIGHT AND LICENSE Copyright (C) 2005 by Chip Turner This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.6.0 or, at your option, any later version of Perl 5 you may have available. =cut