| 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|&|&|go;
$str =~ s|>|>|go;
$str =~ s|<|<|go;
$str =~ s|"|"|go;
$str =~ s|'|'|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.