package App::IniDiff::IniFile; use 5.006; use strict; use Carp; =head1 NAME App::IniDiff::IniFile - perl module to diff and patch .ini files =head1 VERSION Version 0.19 =cut use vars qw($VERSION); $VERSION = '0.19'; =head1 DESCRIPTION IniFile.pm - perl module to diff and patch .ini files =head1 SYNOPSIS This file contains the following: =over 4 =item * package App::IniDiff::IniFile; =item * package App::IniDiff::IniFile::Field; =item * package App::IniDiff::IniFile::Key; =item * package App::IniDiff::IniFile::Filter; =back Creates the following data structure: { 'nextOrderId' => some-number, 'keys' => { { 'name' => "...", 'orderId' => some number, 'fields' => [ { 'name' => "..." 'value' => "...", 'deleted' => 0 or 1, 'annotation' => "...", }, ... ], 'deleted' => 0 or 1, 'annotation' => "...", }, ... }, } =over 4 =item * 'orderId' is used to preserve the order in which keys appear in a file. =item * 'annotation' is used to decorate inidiff output. =item * 'deleted' is used when generating, writing, reading, and applying patch files. =back Some terms used differ from what is normally used in WinINI file-speak =over 4 =item * 'key' is INI 'section' =item * 'field' is INI 'entry' =item * 'field name' is INI 'key' =item * 'field value' is INI 'value' =back =cut use vars qw(&new &write &findKey &keys &removekey &addKey); # added eol to support old end of line \r\n use vars qw($errorString $eol $commentchars); { package App::IniDiff::IniFile::Field; use strict; use Carp; use vars qw(&new &name &canonName &value &deleted &annotation &setFrom &write &canonicalize ); use vars qw($eol); # end of line character - eol $eol = "\n"; sub new { my $proto = shift; confess "new App::IniDiff::IniFile::Field takes 4 arguments" if @_ != 4; my ($name, $value, $del, $annotation) = @_; my $field = { 'name' => $name, 'canonName' => canonicalize($name), 'value' => $value, 'deleted' => $del, 'annotation' => $annotation, }; bless $field, (ref($proto) || $proto); return $field; } sub name { my $field = shift; if (@_) { $field->{'name'} = shift } return $field->{'name'}; } sub canonName { my $field = shift; if (@_) { $field->{'canonName'} = shift } return $field->{'canonName'}; } sub value { my $field = shift; if (@_) { $field->{'value'} = shift } return $field->{'value'}; } sub deleted { my $field = shift; if (@_) { $field->{'deleted'} = shift } return $field->{'deleted'}; } sub annotation { my $field = shift; if (@_) { $field->{'annotation'} = shift } return $field->{'annotation'}; } sub setFrom { my $field = shift; my $from = shift; $field->name($from->name); $field->value($from->value); $field->deleted($from->deleted); $field->annotation($from->annotation); return $field; } sub canonicalize { # Called as object method if (@_ > 0 && ref $_[0]) { return ${$_[0]}->canonName if (@_ == 1); } # Called as object or class method with argument if (@_ == 2) { shift; } elsif (@_ != 1) { confess "wrong number of args" if (@_ != 0); } my $name = $_[0]; $name =~ tr/A-Z/a-z/; return $name; } sub write { my ($field, $fileHandle) = @_; print $fileHandle "; ", $field->annotation, $eol if defined $field->annotation; print $fileHandle $field->name; if ($field->deleted) { print $fileHandle "-"; } elsif (defined $field->value) { print $fileHandle "=", $field->value; } print $fileHandle $eol; } } # End package App::IniDiff::IniFile::Field; { package App::IniDiff::IniFile::Key; use strict; use Carp; use IO::File; use vars qw(&new &name &canonName &orderId &deleted &annotation &fields &canonicalize &findField &addField &appendField &removeField &write ); use vars qw($eol); # end of line character - eol $eol = "\n"; sub new { my $proto = shift; confess "new App::IniDiff::IniFile::Key takes 3 arguments" if @_ != 3; my ($name, $del, $annotation) = @_; my $key = { 'name' => $name, 'canonName' => canonicalize($name), 'orderId' => undef, 'deleted' => $del, 'annotation' => $annotation, 'fields' => [], }; bless $key, (ref($proto) || $proto); return $key; } sub name { my $key = shift; # Do not change canonName (see kludge in App::IniDiff::IniFile::new regarding patches) if (@_) { $key->{'name'} = shift } return $key->{'name'}; } sub canonName { my $key = shift; if (@_) { $key->{'canonName'} = shift } return $key->{'canonName'}; } sub orderId { my $key = shift; if (@_) { $key->{'orderId'} = shift } return $key->{'orderId'}; } sub deleted { my $key = shift; if (@_) { $key->{'deleted'} = shift } return $key->{'deleted'}; } sub annotation { my $key = shift; if (@_) { $key->{'annotation'} = shift } return $key->{'annotation'}; } sub fields { my $key = shift; confess "too many args" if @_ > 0; return $key->{'fields'}; } sub canonicalize { # Called as object method if (@_ > 0 && ref $_[0]) { return ${$_[0]}->canonName if (@_ == 1); } # Called as object or class method with argument if (@_ == 2) { shift; } elsif (@_ != 1) { confess "wrong number of args" if (@_ != 0); } my $name = $_[0]; $name =~ tr/A-Z/a-z/; return $name; } # Note: only finds first field... sub findField { my ($key, $fieldName) = @_; $fieldName = App::IniDiff::IniFile::Field->canonicalize($fieldName); my ($field); foreach $field (@{$key->fields}) { return $field if ($field->canonName eq $fieldName); } return undef; } sub addField { my $key = shift; my $field = shift; my $xfield = $key->findField($field->name); if (defined $xfield) { $xfield->setFrom($field); return $xfield; } return $key->appendField($field); } sub appendField { my $key = shift; my $field = shift; push(@{$key->fields}, $field); return $field; } # # Remove a field, either by name or by reference # sub removeField { my $found = 0; my ($key, $arg) = @_; if (ref $arg) { my $fieldToast = $arg; for (my $i = 0 ; $i < @{$key->fields} ; $i++) { my $field = ${$key->fields}[$i]; if ($field eq $fieldToast) { splice(@{$key->fields}, $i, 1); $i--; $found++; } } } else { my $fieldName = App::IniDiff::IniFile::Field->canonicalize($arg); for (my $i = 0 ; $i < @{$key->fields} ; $i++) { my $field = ${$key->fields}[$i]; if ($field->canonName eq $fieldName) { splice(@{$key->fields}, $i, 1); $i--; $found++; } } } return $found; } sub write { my ($key, $fileHandle) = @_; my ($del) = $key->deleted ? '-' : ''; print $fileHandle "; ", $key->annotation, $eol if defined $key->annotation; print $fileHandle "[", $key->name, "]", $del, $eol; if (!$key->deleted) { my $field; foreach $field (@{$key->fields}) { $field->write($fileHandle); } } print $fileHandle $eol; } } # End package App::IniDiff::IniFile::Key; # $IniFile package Globals $errorString = undef; # end of line character - eol $eol = "\n"; $commentchars = ';#'; # Allow DOS and Unix style comment. sub new { my $proto = shift; my $ini = { # Used to generate monotonically increasing key ids - used # to reserve order of ini file. 'nextOrderId' => 0, 'keys' => {}, }; bless $ini, (ref($proto) || $proto); return $ini if (@_ == 0); confess "new called with too many arguments" if (@_ > 4); my ($fileHandle, $isPatch, $addM, $stripComments) = @_; # Patches: allow duplicate key names my ($key); my $ok = 1; $isPatch = 0 if !defined $isPatch; $addM = 0 if !defined $addM; # add ^M if pre-NT $stripComments = 0 if ! defined $stripComments; # strip out trailing inline comments having semicolon # comment out stripComments if isPatch # - this may be a problem # - leave them in unless specifically asked # $stripComments = 1 if $isPatch; # always strip out comments from patches if ($addM == 1) { $eol = "\r\n"; # set the children's eol members to this value as well ... # not using set() methods - oh dear $App::IniDiff::IniFile::Key::eol = "\r\n"; $App::IniDiff::IniFile::Field::eol = "\r\n"; } while (<$fileHandle>) { chomp; # # Strip comments - not in key names ([...]) and not in strings.. # # if a [key] section if (/^\s*(\[[^]]+])(.*)$/) { my ($key, $rest) = ($1, $2); # do care if it strips comments after [key] section if ($stripComments) { $rest =~ s/[$commentchars].*//; } $_ = $key . $rest; } # not a comment starting with ; and has a " in it elsif (!/^\s*[$commentchars]/ && /"/) { # Slow, but perl won't go exponential... my $line = ''; # walk through non comments and matched quoted strings # from left to right # until we hit a comment character or a non matched quote while (/^([^"$commentchars]*"[^"]*")(.*)/) { $line .= $1; $_ = $2; } # Some (burnt) ini files have unmatched quotes... rather # than toast these, we assume they have no comments. if (!/"/) { # do not strip comments unless requested if ($stripComments) { s/[$commentchars].*//; } } $_ = $line . $_; } elsif ($stripComments) { s/[$commentchars].*//; } s/\s*$//; # remove trailing space includes \r... # not skipping blank lines at this point results in errors when diffing next if /^$/; # skip blank lines if (/^\s*\[([^]]+)](-?)$/) { my ($name, $del) = ($1, $2); if ($isPatch) { # Patches are a bit strange as there can be duplicate # key names - to deal with this, the canonName (hash index) # is a generated (unique) thing and we fix up the real name # after the key is created. $key = $ini->addKey( new App::IniDiff::IniFile::Key( "[$ini->{'nextOrderId'}]", $del eq '-', undef)); $key->name($name); } else { if ($del eq '-') { $errorString = "$.: non-patch file has deleted key"; $ok = 0; last; } if ($ini->findKey($name)) { $errorString = "$.: duplicate key: $name"; $ok = 0; last; } $key = $ini->addKey(new App::IniDiff::IniFile::Key($name, $del eq '-', undef)); } next; } # passed through, this is not a key, so it is something else my ($name, $value); # fix quotes if (/"/) { # Slow, but perl won't go exponential... my $line = ''; # cycle through matched pairs of quotes in LHS $name # accepts = between pair of quotes in LHS while (/^([^"=]*"[^"]*")(.*)$/) { $line .= $1; $_ = $2; } # no remaining quotes in LHS # allows unmatched quote in RHS if (/^([^"]*)=(.*)$/) { $name = $line . $1; $value = $2; } elsif (/"/) { # Wonder if this will be a problem... $errorString = "$.: unmatched quote (no preceeding =)"; $ok = 0; # quits here ... maybe we shouldn't quit parsing last; } else { $name = $line . $_; $value = undef; } } elsif (/^([^=]+)=(.*)$/) { $name = $1; $value = $2; } else { $name = $_; $value = undef; } my $del = 0; if (!defined $value && $name =~ /-$/) { if (!$isPatch) { $errorString = "$.: non-patch file has deleted field"; $ok = 0; last; } $del = 1; chop $name; } if (!defined $key) { $errorString = "$.: field outside of key\n"; $ok = 0; last; } if ($key->deleted) { $errorString = "$.: deleted key has field\n"; $ok = 0; last; } # when a comment precedes a new key (section), # it gets stuck with the previous one # because the blank line at the end of a key section # must get eaten for inidiff to work $key->appendField(new App::IniDiff::IniFile::Field($name, $value, $del, undef)); } return undef if !$ok; return $ini; } sub write { my ($ini, $fileHandle) = @_; my ($key); foreach $key (@{$ini->keys}) { $key->write($fileHandle); } } sub findKey { my ($ini, $keyName) = @_; return $ini->{'keys'}->{App::IniDiff::IniFile::Key->canonicalize($keyName)}; } sub keys { my ($ini) = @_; return [ sort { $a->orderId <=> $b->orderId } values(%{$ini->{'keys'}}) ]; } # # Remove a key, either by name or by reference # sub removeKey { my ($ini, $arg) = @_; if (ref $arg) { my $keyToast = $arg; my ($name, $key); while (($name, $key) = each %{$ini->{'keys'}}) { if ($key eq $keyToast) { delete $ini->{'keys'}->{$name}; } } } else { my $keyName = $arg; return delete $ini->{'keys'}->{App::IniDiff::IniFile::Key->canonicalize($keyName)}; } } sub addKey { my ($ini, $key) = @_; $ini->{'keys'}->{$key->canonName} = $key; $key->orderId($ini->{'nextOrderId'}++); return $key; } { package App::IniDiff::IniFile::Filter; use strict; use Carp; use IO::File; use vars qw(&new &readConf &filter &export); use vars qw($errorString); sub new { my $proto = shift; confess "new App::IniDiff::IniFile::Filter takes no arguments" if @_ != 0; my $field = { 'keyFilters' => [] }; bless $field, (ref($proto) || $proto); return $field; } sub readConf { my ($filter, $file) = @_; my $in = new IO::File $file, "r"; if (!defined $in) { $errorString = "can't open $file - $!"; return 0; } my ($keyActions) = undef; my ($entryActions) = undef; while (<$in>) { next if (/^\s*(#|$)/); # Trim whitespace s/^\s+//; s/\s+$//; # include another filter file? if (/^\s*include\s+"([^"]*)"\s*$/) { my ($ifile) = $1; # End the previous key (with error checking) # added {} around keyActions if (defined $keyActions && !@{$keyActions}) { $errorString = "$file:$.: previous key has no actions"; $in->close; return 0; } # added {} around entryActions elsif (defined $entryActions && !@{$entryActions}) { $errorString = "$file:$.: previous name/value pattern has no actions"; $in->close; return 0; } $keyActions = $entryActions = undef; # If not an absolute path, try relative to this file first. my ($mypath) = $file; $mypath =~ s:/+[^/]*$::; if ($ifile !~ /^\// && $mypath ne '' && $mypath ne $file && -e $mypath."/".$ifile) { return 0 if (!$filter->readConf($mypath."/".$ifile)); } else { return 0 if (!$filter->readConf($ifile)); } next; } # A new key? if (/^\[(.+)](|\s*-)$/) { my ($keyPat, $isDel) = ($1, $2 eq '' ? 0 : 1); if (defined $keyActions && !@{$keyActions}) { # added {} $errorString = "$file:$.: previous key has no actions"; $in->close; return 0; } elsif (defined $entryActions && !@{$entryActions}) { # added {} $errorString = "$file:$.: previous name/value pattern has no actions"; $in->close; return 0; } push( @{$filter->{'keyFilters'}}, { 'keyPat' => $1, 'deleteAll' => $isDel, 'keyActions' => [] }); # ${$filter} is not an array # $keyActions = $isDel ? undef : # ${$filter}[$#$filter]->{'keyActions'}; my @filterHashes = @{$filter->{'keyFilters'}}; my $numHashes = $#filterHashes; $keyActions = $isDel ? undef : $filterHashes[$numHashes]->{'keyActions'}; next; } # A new entry match operator? if (/^\s*(name|value)\s+(\S.*)$/) { my ($matchOn, $matchPat) = ($1, $2); if (!defined $keyActions) { $errorString = "$file:$.: name/value pattern found outside key"; $in->close; return 0; } if (defined $entryActions && !@{$entryActions}) { # added {} $errorString = "$file:$.: previous name/value pattern has no actions"; $in->close; return 0; } $entryActions = []; # added {} to keyActions push( @{$keyActions}, { 'matchOn' => $matchOn, 'matchPat' => $matchPat, 'entryActions' => $entryActions, }); next; } # A entry substitution if (/^\s*subst\s+(name|value)\s+(\S.*)$/) { my ($action, $subst) = ($1, $2); if (!defined $keyActions) { $errorString = "$file:$.: substitution found outside key"; $in->close; return 0; } if (!defined $entryActions) { $errorString = "$file:$.: substitution found outside entry/key pattern"; $in->close; return 0; } # added {} to entryActions push( @{$entryActions}, { 'action' => "subst\u$action", 'subst' => $subst, }); next; } # An entry deletion if (/^\s*delete$/) { if (!defined $keyActions) { $errorString = "$file:$.: delete entry found outside key"; $in->close; return 0; } if (!defined $entryActions) { $errorString = "$file:$.: delete entry found outside entry/key pattern"; $in->close; return 0; } # added {} to entryActions push( @{$entryActions}, { 'action' => 'delete', 'subst' => undef, }); next; } $errorString = "$file:$.: unexpected line"; $in->close; return 0; } $in->close; return 1; } # # Given a ini object, modify it by applying the filtering commands # (deletions and substitutions) contained in the filter object. # Returns true iff there are no problems. # sub filter { my ($filter, $ini) = @_; my $key; foreach $key (@{$ini->keys}) { # was unblessed references my $keyFilt; foreach $keyFilt (@{$filter->{'keyFilters'}}) { next if ($key->name !~ /^$keyFilt->{'keyPat'}$/i); if ($keyFilt->{'deleteAll'}) { $ini->removeKey($key); next; # no point in going on... } # remove end block here $keyFilt was not defined # Must be entry substituions/deletions my $keyAction; foreach $keyAction (@{$keyFilt->{'keyActions'}}) { my $field; foreach $field (@{$key->fields}) { my $target = $keyAction->{'matchOn'} eq 'name' ? $field->name : $field->value; next if ($target !~ /^$keyAction->{'matchPat'}$/i); # # Have a match - carry out entry actions # my $entryAction; foreach $entryAction (@{$keyAction->{'entryActions'}}) { if ($entryAction->{'action'} eq 'substName') { my $name = $field->name; eval "\$name =~ $entryAction->{'subst'}"; if ($@ ne '') { $errorString = "error substituting " . "$target using $keyAction->{'subst'}"; return undef; } $field->name($name); } elsif ($entryAction->{'action'} eq 'substValue') { my $value = $field->value; eval "\$value =~ $entryAction->{'subst'}"; if ($@ ne '') { $errorString = "error substituting " . "$target using $keyAction->{'subst'}"; return undef; } $field->value($value); } elsif ($entryAction->{'action'} eq 'delete') { $key->removeField($field); last; } else { $errorString = "inifilter::filter: internal " . "error - unknown entry action: " . "$entryAction->{'action'}"; return undef; } } } } } } return 1; } # End Sub Filter # # Export all filtering commands to the command line (mainly for DEBUG) # (deletions and substitutions) contained in the filter object. # Returns true iff there are no problems. # sub export { my ($filter) = @_; my $keyFilt; foreach $keyFilt (@{$filter->{'keyFilters'}}) { print "keyPat = " . $keyFilt->{'keyPat'} . "\n"; if ($keyFilt->{'deleteAll'}) { print "deleteAll = " . $keyFilt->{'deleteAll'} . "\n"; } # remove end block here $keyFilt was not defined # Must be entry substituions/deletions my $keyAction; foreach $keyAction (@{$keyFilt->{'keyActions'}}) { print "matchOn = " . $keyAction->{'matchOn'} . "\n"; print "matchPat = " . $keyAction->{'matchPat'} . "\n"; # # Have a match - carry out entry actions # my $entryAction; foreach $entryAction (@{$keyAction->{'entryActions'}}) { print "entryAction = " . $entryAction->{'action'} . "\n"; if ($entryAction->{'action'} eq 'substName') { print "subst name = " . $entryAction->{'subst'} . "\n"; } elsif ($entryAction->{'action'} eq 'substValue') { print "subst value = " . $entryAction->{'subst'} . "\n"; } elsif ($entryAction->{'action'} eq 'delete') { print "delete = " . $entryAction->{'subst'} . "\n"; last; } else { $errorString = "inifilter::export: internal error " . "- unknown entry action: $entryAction->{'action'}"; return undef; } } } } return 1; } # End Sub Export } # End package App::IniDiff::IniFile::Filter =pod =head1 AUTHOR Michael Rendell, Memorial University of Newfoundland =head1 MAINTAINERS Jeremy Squires =head1 SOURCE =over 4 =item * The source for this package is available here: L =back =head1 ACKNOWLEDGEMENTS Michael Rendell, Memorial University of Newfoundland produced the first version of the Regutils package from which this package was derived. =over 4 =item * It is still available from: L =back =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes. =head1 SUPPORT You can find documentation for this module with the perldoc command. perldoc App::IniDiff::IniFile You can also look for information at: =over 4 =item * RT: CPAN's request tracker (report bugs here) L =item * AnnoCPAN: Annotated CPAN documentation L =item * CPAN Ratings L =item * Search CPAN L =back =head1 LICENSE AND COPYRIGHT This software is Copyright (c) 1998 Memorial University of Newfoundland This is free software, licensed under: The GNU General Public License, Version 3, July 2007 See F =cut 1; # End of App::IniDiff::IniFile