channelping almost midnight

Parse::INI convert ini file to perl data structure or XML

package Parse::INI;

use strict;

use Tie::IxHash;

sub new
{
  my $self = shift;

  $self = bless { @_ }, ref($self) || $self;

  $self->{'use_perl_booleans'} = 0 unless exists($self->{'use_perl_booleans'});

  $self->{'element_name_change_count'} = 0;

  return $self;
}

sub ini2xml
{
  my $self = shift;
  my %params = @_;

  # required
  my $filename = $params{'input'} || die qq[\n\tError: 'input' filename missing.\n\n];

  # optional
  my $output_filename = $params{'output'}   || undef;
  my $encoding        = $params{'encoding'} || undef;
  my $root_tag        = $params{'root'}     || 'Sections';
  my $format          = $params{'format'}   || 'elements';

  unless ($format =~ m!^(attributes|elements)$!)
  {
    die qq[\n\tformat [$format] is not valid.\n\n];
  }

  my $encoding_string = ($encoding) ? qq[ encoding="$encoding"] : '';

  my $data = $self->parse($filename);

  my ($esc_section, $esc_key, $esc_value);

  my $xml = qq[<?xml version="1.0"?${encoding_string}>\n\n<${root_tag}>\n\n];

  if ($format eq 'attributes')
  {
    for my $section (keys %$data)
    {
      $esc_section = Parse::INI::escape($section);

      $xml .= qq[  <section name="$esc_section">\n];

      for my $key (keys %{$data->{$section}})
      {
        $esc_key = Parse::INI::escape($key);
        $esc_value = Parse::INI::escape($data->{$section}->{$key});
        $xml .= qq[    <item key="$esc_key" value="$esc_value" />\n];
      }

      $xml .= qq[  </section>\n\n];
    }
  }

  elsif ($format eq 'elements')
  {
    for my $section (keys %$data)
    {
      $esc_section = Parse::INI::escape($section);

      $self->legalize_element_name(\$esc_section);

      $xml .= qq[  <$esc_section>\n];

      for my $key (keys %{$data->{$section}})
      {
        $esc_key = Parse::INI::escape($key);
        $esc_value = Parse::INI::escape($data->{$section}->{$key});

        $self->legalize_element_name(\$esc_key);

        if (length($esc_value))
        {
          $xml .= qq[    <$esc_key>$esc_value</$esc_key>\n];
        }
        else
        {
          $xml .= qq[    <$esc_key />\n];
        }
      }

      $xml .= qq[  </$esc_section>\n\n];
    }
  }

  if ($self->{'element_name_change_count'})
  {
    my $ct = $self->{'element_name_change_count'};
    my $s = ($ct >= 2) ? 's' : '';
    $xml .= qq[  <!-- $ct element${s} required name change to achieve well-formedness. -->\n\n];
  }

  $xml .= qq[</${root_tag}>\n];

  if ($output_filename)
  {
    open(XML, ">$output_filename") || die qq[\n\tcannot open file for writing $!\n\n];
    print XML $xml;
    close(XML);
  }
  else
  {
    print $xml;
  }

  return $xml;
} # end ini2xml

# XML requires element names to match a specific format: Element names
# must start with a letter or an underscore.  The rest of the
# element's name can contain any number of letters, numbers, hyphens,
# periods, or underscores. Any characters that don't match that set
# will be replaced with an underscore.  The element name will be
# prepended with an underscore if need be.
sub legalize_element_name
{
  my $self = shift;
  my $str = shift;

  my $count1 = 0;
  my $count2 = 0;

  # first replace non-legal characters with underscores
  ($count1) = $$str =~ s/[^a-zA-Z0-9\._-]/_/go;

  # prepend element name with underscore if first letter is not a
  # letter or underscore
  ($count2) = $$str =~ s/^([^a-zA-Z_])/_$1/o;

  if ($count1 || $count2)
  {
    ++$self->{'element_name_change_count'};
  }

  return;
}

# the five characters that need to be escaped
sub escape
{
  my $str = shift;

  $str =~ s|&|&amp;|go;
  $str =~ s|>|&gt;|go;
  $str =~ s|<|&lt;|go;
  $str =~ s|"|&quot;|go;
  $str =~ s|'|&apos;|go;

  return $str;
}

sub parse
{
  my $self = shift;
  my $filename = shift;

  $filename || die(qq[\n\tfilename arg required $!\n\n]);

  # open file read only
  open(IN, $filename) || die(qq[\n\tcannot find file '$filename' $!\n\n]);

  my $k;
  my $v;
  my $count = 0;
  my $section_name = '';
  my $data = {}; tie(%$data, 'Tie::IxHash');
  while (<IN>)
  {
    chomp;

    # skip blank lines and comments
    next if /^\s*$/o; next if /^;/o;

    # it's either a section heading or a key=value pair
    if (/^\[\s*([^\]]+)\]\s*/o)
    {
      $section_name = $1;
      $data->{$section_name} = {};
    }
    else
    {
      # parse the k v pair
      #
      # key=value pairs in ini files have one part of the string in
      # which white space is significant: the part between the
      # equals sign and the first character of the value, e.g.:
      #
      #     AppName = WriteOnceCrashEverywhere
      #              ^
      #
      # The space just before the 'W' is not trimmed; it is
      # preserved as part of the value: " WriteOnceCrashEverywhere"
      #
      # Trailing white-space for both key and value are trimmed.
      ($k, $v) = split(/\s*=/o, $_, 2);

      # trim trailing white-space
      $v =~ s/\s+$//o;

      # 'use_perl_booleans' set to true (1)
      # "true" and "false" are common ini file values, but perl will
      # evaluate both strings as true; therefore, we convert those
      # boolean string expression into their perl equivalents: 1 or 0
      if ($self->{'use_perl_booleans'})
      {
        if ($v =~ /^(true|false)$/io)
        {
          $v = ('true' eq lc($1)) ? 1 : 0;
        }
      }

      $data->{$section_name}->{$k} = $v;
    }

  }
  close(IN);

  return $data;
}

1;

__END__

=head1 NAME

Parse::INI - read ini file into a perl data structure or convert directly to XML

=head1 SYNOPSIS

  Example 1: put ini file's parameters in a perl data structure:

    use Parse::INI;
    $p = Parse::INI->new();

    $configdata = $p->parse('filename.ini');


  Example 2: convert directly to XML:

    use Parse::INI;
    $p = Parse::INI->new();

    $xml = $p->ini2xml(
                       input     => 'filename.ini',  # required
                       output    => 'test.xml',      # optional (default: print to console)
                       root      => 'NetMapHistory', # optional (default: 'Sections')
                       encoding  => 'US-ASCII',      # optional (default: no encoding attribute)
                       format    => 'attributes',    # optional (default: 'elements')
                      );


=head1 DESCRIPTION

Parse::INI reads in Windows style ini files and puts parameters into a
perl data structure; ini file's sections are preserved.

Two methods are public: parse() and ini2xml().  parse() returns a data
structure.  xml2ini() has 1 required argument (input filename) and
several optional arguments, explained in the synopsis section of this
document.

Tie::IxHash package is required.


=head1 AUTHOR

Gerald Gold <gold@channelping.com>

=head1 COPYRIGHT

Copyright (c) 2004-Eternity Gerald Gold and channelping.  All rights
reserved.  This class is free software; you may redistribute it and/or
modify it under the same terms as Perl itself.

^