e-smith-lib/root/usr/share/perl5/vendor_perl/esmith/templates.pm

1092 lines
32 KiB
Perl

#----------------------------------------------------------------------
# Copyright 1999-2007 Mitel Networks Corporation
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#----------------------------------------------------------------------
package esmith::templates;
use strict;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT = qw(processTemplate);
our @EXPORT_OK = qw(removeBlankLines);
use Text::Template 'fill_in_file';
use Errno;
use esmith::config;
use esmith::db;
use vars '$TEMPLATE_COUNT';
use Carp;
use File::Basename;
use File::stat;
use FileHandle;
use DirHandle;
$TEMPLATE_COUNT = 0;
=for testing
use_ok('esmith::templates');
=head1 NAME
esmith::template - Utilities for e-smith server and gateway development
=head1 VERSION
This file documents C<esmith::template> version B<1.7.0>
=head1 SYNOPSIS
use esmith::template;
processTemplate(...);
=head1 DESCRIPTION
This is the interface to the E-Smith templating system. For an
overview of how the system works, see section "3.4 Templated
Configuration System" of the Dev Guide.
esmith::template exports a single function, processTemplate, which, as
you might guess, processes sets of templates into a single output
file.
=head2 Template Variables
The following variables are available to all templates.
=over 4
=item B<$confref>
B<DEPRECATED>. Contains a reference to the hash passed in via
CONFREF. If none was given it defaults to a tied esmith::config hash.
=item B<$DB>
Contains a reference to an esmith::ConfigDB object pointing at the
default configurations. This is to be used to call methods like
C<$DB->services> and *not* for alterting the database.
=back
In addition, each record in the default esmith configuration database
(configuration) is available as a hash if it has
multiple properties (where each key/value is a property of the record)
or if it has a single property (type) then it given as a scalar.
So you can say:
{ $DomainName } # $configdb->get('DomainName')->value;
{ $sshd{status} } # $configdb->get('sshd')->prop('status')
Finally, variables from additional databases are usually gotten
via the esmith::DB->as_hash feature.
{ require esmith::HostsDB;
my %Hosts = esmith::HostsDB->as_hash;
...
}
=head2 Functions
=over 4
=item B<processTemplate>
processTemplate({ CONFREF => \%config,
TEMPLATE_PATH => $output_file
});
$filled_in_template = processTemplate({ CONFREF => \%config,
TEMPLATE_PATH => $output_file
OUTPUT_TYPE => 'string'
});
processTemplate() expands a set of templates based on the keys/values
in %config.
The options to processTemplate are as follows...
=over 4
=begin deprecated
=item ALL_RECORDS_AS_SCALARS
For backwards compatibility purposes, the expand-template script needs
I<all> keys to be scalars, whether they have multiple properties or
not. If this variable is true all the variables pulled in from
esmith::ConfigDB->open will be scalars in addition to hashes.
This is B<ONLY> to be used by expand-template.
=end deprecated
=item MORE_DATA
A hash ref containing additional variables you'd like to put into the
template. Key is the name of the variable, value is it's value.
# $Foo = 'bar'
MORE_DATA => { Foo => "bar" }
Any keys in MORE_DATA will override those from the default
esmith::ConfigDB.
This replaces I<CONFREF>.
=item CONFREF
B<DEPRECATED>. A reference to the hash which will become the
variables in the template. So $config{Foo} becomes $Foo. In
addition, there is the $confref variable which contains a reference
back to the original CONFREF.
This is usually a tied esmith::config hash.
This has been replaced by MORE_DATA and cannot be used in conjunction.
=begin testing
use esmith::HostsDB;
eval {
processTemplate({ CONFREF => { foo => "bar" },
MORE_DATA => { something => 'other' },
});
};
like( $@, qr/^ERROR: Can't use CONFREF with MORE_DATA/ );
=end testing
=item TEMPLATE_PATH
Full path to the file which fill result from this template. For
example, '/etc/hosts'.
=item TEMPLATE_EXPAND_QUEUE
List of directories to scan for templates. If not specified it
defaults to:
/etc/e-smith/templates-custom
/etc/e-smith/templates
it then appends the TEMPLATE_PATH to this, so the resulting search
might be:
/etc/e-smith/templates-custom/etc/host
/etc/e-smith/templates/etc/host
All templates found are combined in ASCIIbetical order to produce the
final file. The exception to this is template-begin, which always
comes first, and template-end, which always comes last.
If no template-begin is found the one in
/etc/e-smith/templates-default/ will be used.
If two directories contain the same template those eariler in the
queue will override those later. So /etc/e-smith/templates-custom/foo
will be used instead of /etc/e-smith/templates/foo.
=item OUTPUT_PREFIX
Directory which contains the OUTPUT_FILENAME.
=item OUTPUT_FILENAME
The file which results from this template.
Defaults to the TEMPLATE_PATH.
=item FILTER
A code ref through which each line of the resulting text is fed, for
example:
FILTER => sub { "# $_[0]" }
would put a # in front of each line of the template.
FILTER => sub { $_[0] =~ /^\s*$/ ? '' : $_[0] }
will remove all lines that contain only whitespace.
=item UID
=item GID
The user and group ID by which the resulting file should be owned.
This obviously means you have to run procTemplate as root.
Defaults to UID 0 and GID 0.
=item PERMS
File permissions which the resulting file should be set to have.
Defaults to 0644.
=item OUTPUT_TYPE
Determines if the filled in template should go straight to a file or
be returned by processTemplate(). The values can be:
string return the filled in template
file write it to disk
Defaults to 'file'
=back
For example we have a template F</etc/e-smith/templates/etc/hosts>
that we want to expand to F</etc/hosts> using the normal
configuration.
# Records from esmith::ConfigDB->open will be available by default
processTemplate({
TEMPLATE_PATH => '/etc/hosts',
});
Example 2: we have a template F</etc/e-smith/templates-user/qmail>
that we want to expand to F</home/e-smith/files/users/$username/.qmail>
Solution:
processTemplate({
TEMPLATE_PATH => '/qmail',
TEMPLATE_EXPAND_QUEUE => [
'/etc/e-smith/templates-user-custom',
'/etc/e-smith/templates-user',
],
OUTPUT_PREFIX => '/home/e-smith/files/users/$username',
OUTPUT_FILENAME => '.qmail',
FILTER => sub { $_[0] =~ /^\s*$/ ? '' : $_[0] },
UID => $username,
GID => $username,
PERMS => 0644,
});
Example 3: we have a template fragment
F</etc/e-smith/templates/etc/httpd/conf/httpd.conf/80VirtualHosts>
that needs to iterate through the given list of VirtualHosts,
process each template and return the results in a string until all the
VirtualHosts have been completed. The results will be expanded
into the F</etc/httpd/conf/httpd.conf> file.
Solution: In the 80VirtualHosts fragment, we use the OUTPUT_TYPE='string'
option to return the output of processTemplate for each VirtualHost as a
string, and then we add the results to the $OUT variable for inclusion in the
httpd.conf template expansion. We store the VirtualHosts template in
F</etc/httpd/conf/httpd.conf/VirtualHosts> for clarity and namespace
separation.
foreach my $ipAddress (keys %ipAddresses)
{
# the $OUT variable stores the output of this template fragment
use esmith::templates;
$OUT .= processTemplate (
{
MORE_DATA => { ipAddress => $ipAddress, port => $port,
virtualHosts => \@virtualHosts,
virtualHostContent => \%virtualHostContent },
TEMPLATE_PATH => "/etc/httpd/conf/httpd.conf/VirtualHosts",
OUTPUT_TYPE => 'string',
});
}
=cut
sub processTemplate {
######################################
# set the default values to use if not
# specified in parameters
# every valid parameter should have a default
######################################
my %defaults = (
MORE_DATA => {},
ALL_RECORDS_AS_SCALARS => 1,
CONFREF => undef,
TEMPLATE_PATH => '', # replaces FILE_PATH
OUTPUT_FILENAME => '', # replaces FILE_PATH_LIST
TEMPLATE_EXPAND_QUEUE =>
[ '/etc/e-smith/templates-custom', '/etc/e-smith/templates', ],
OUTPUT_PREFIX => '', # replaces TARGET
FILTER => undef,
UID => 0,
GID => 0,
PERMS => 0644,
OUTPUT_TYPE => 'file', # [file|string]
DELETE => 0,
);
# store the valid output types so we can do a quick sanity check
my @valid_output_types = ( 'file', 'string' );
my $conf_or_params_ref = shift;
my $path = shift;
my %params_hash;
if ( defined $path ) {
# This is the old syntax, so we just grab the the two or maybe
# three parameters ...
%params_hash = (
CONFREF => $conf_or_params_ref,
TEMPLATE_PATH => $path,
);
if ( my $source = shift ) {
$params_hash{'TEMPLATE_EXPAND_QUEUE'} = [$source];
}
}
else {
%params_hash = %$conf_or_params_ref;
}
# Read additional metadata assocated with the templated file
my $metadata_path = "/etc/e-smith/templates.metadata/$params_hash{TEMPLATE_PATH}";
if (open(FILE, $metadata_path))
{
while (<FILE>)
{
/^([^=]+)=(.*)$/;
$params_hash{$1} = eval $2;
}
close(FILE);
}
if (my $d = DirHandle->new($metadata_path))
{
while ($_ = $d->read)
{
# skip any directories, including . and ..
next if -d "$metadata_path/$_";
# Untaint filename
/(\w+)/; my $file = $1;
unless (open(FILE, "$metadata_path/$file"))
{
warn("Could not open metadata file $metadata_path/$file: $!");
next;
}
# Read and untaint content of file
$params_hash{$file} = eval do { local $/; $_ = <FILE>; /(.*)/s ; "{ $1 }" };
close(FILE);
}
}
# warn on deprecated or unknown parameters
foreach my $key ( keys %params_hash ) {
unless ( exists $defaults{$key} ) {
carp "WARNING: Unknown parameter '$key' "
. "passed to processTemplate\n";
}
}
# Check for illegal combinations of variables.
if ( exists $params_hash{CONFREF} && exists $params_hash{MORE_DATA}) {
carp "ERROR: Can't use CONFREF with MORE_DATA in processTemplate\n";
return;
}
### merge incoming parameters with the defaults
# -this is backwards compatible with the old positional
# parameters $confref, $filename, and $source
my %p = ( %defaults, %params_hash );
# set OUTPUT_FILENAME to TEMPLATE_PATH if it wasn't explicitly set
unless ( $p{'OUTPUT_FILENAME'} ) {
# if OUTPUT_FILENAME exists, it holds an array of target filenames
$p{'OUTPUT_FILENAME'} = $p{'TEMPLATE_PATH'};
}
unless ( exists $p{'TEMPLATE_PATH'} ) {
carp "ERROR: TEMPLATE_PATH parameter missing in processTemplate\n";
return;
}
my $template_path = $p{'TEMPLATE_PATH'};
my $outputfile = $p{'OUTPUT_PREFIX'} . '/' . $p{'OUTPUT_FILENAME'};
my $tempfile = "$outputfile.$$";
# sanity check on OUTPUT_TYPE
unless ( grep( $p{'OUTPUT_TYPE'}, @valid_output_types ) ) {
carp
"ERROR: Invalid OUTPUT_TYPE parameter passed to processTemplate\n";
return;
}
# If OUTPUT_TYPE=file and FILTER is off, then $fh is the output filehandle.
# If OUTPUT_TYPE=file and FILTER is on, then $ofh is the real output
# filehandle, and $fh is a temporary file for the pre-filtered output.
my $fh;
my $ofh;
# if OUTPUT_TYPE=string, then $text is the output string
my $text;
if ( $p{'OUTPUT_TYPE'} eq 'file' ) {
##########################################################
# open the target file before servicing the template queue
##########################################################
if ( -d "$outputfile" ) {
carp "ERROR: Could not expand $outputfile template "
. "- it is a directory\n";
return;
}
# delete the file and do no more if we're told to by metadata
if ($p{'DELETE'})
{
unlink "$outputfile";
return;
}
# use POSIX::open to set permissions on create
require POSIX;
my $fd =
POSIX::open( $tempfile,
&POSIX::O_CREAT | &POSIX::O_WRONLY | &POSIX::O_TRUNC, 0600);
unless ($fd)
{
carp "ERROR: Cannot create output file " . "$tempfile $!\n";
return;
}
# create a filehandle reference to the newly opened file
$fh = new FileHandle;
unless ($fh->fdopen( $fd, "w" ))
{
carp "ERROR: Cannot open output file " . "$tempfile: $!\n";
return;
}
if ( defined $p{FILTER} ) {
# We have a filter to apply to the output. So we write the output
# into an anonymous file, to prepare it for postprocessing
require IO::File;
$ofh = $fh;
$fh = IO::File->new_tmpfile;
}
}
# Construct a hash containing mapping each template fragment
# to its path. Subsequent mappings of the same fragment
# override the previous fragment (ie: merge new fragments
# and override existing fragments)
# use queue to store template source directories in order
my @template_queue = @{ $p{'TEMPLATE_EXPAND_QUEUE'} };
# use a hash to store template fragments
my %template_hash = _merge_templates( $template_path, @template_queue );
# if template hash is empty produce an error
unless ( keys %template_hash ) {
unlink $tempfile;
carp "ERROR: No templates were found for $template_path.\n";
return;
}
#####################################################
# Process the template fragments and build the target
#####################################################
# create unique package namespace for this template
# namespace is used by all template fragments
$TEMPLATE_COUNT++;
my $pkg = "esmith::__TEMPLATE__::${TEMPLATE_COUNT}";
# Setup the template variables.
my $tmpl_vars = _init_tmpl_vars( \%p );
my $errorCount = 0;
my $warningCount = 0;
my $debug_template_expansion =
( $$tmpl_vars[0]{processTemplate}{Debug} || 'no' ) eq 'yes';
# expand the template fragments into the target file
foreach my $key ( sort _template_order keys %template_hash ) {
my $filepath = $template_hash{$key};
# Text::Template doesn't like zero length files so skip them
unless ( -s $filepath ) { next }
$debug_template_expansion
&& print "DEBUG: Expanding template fragment $filepath\n";
local $SIG{__WARN__} = sub {
$warningCount++;
print STDERR "WARNING in $filepath: $_[0]";
};
{
# prime the package namespace
# use statements will only be run once per template
# XXX DEPRECATED!
eval "
package $pkg;
use esmith::db;
use esmith::util;
";
# Arcane Text::Template error passing. Don't ask.
my $broken = sub {
my %args = @_;
( my $error = $args{error} ) =~ s/\n+\z//;
my $text = $args{text};
my $lineno = $args{lineno};
$errorCount++;
print STDERR "ERROR in $filepath: "
. "Program fragment delivered error <<$error>>"
. " at template line $lineno\n";
return "";
};
# process the templates
if ( $p{'OUTPUT_TYPE'} eq 'file' ) {
unless (fill_in_file(
"$filepath",
HASH => $tmpl_vars,
PACKAGE => $pkg,
BROKEN => $broken,
UNTAINT => 1,
OUTPUT => \*$fh
))
{
carp "ERROR: Cannot process template $filepath: $Text::Template::ERROR\n";
return;
}
}
elsif ( $p{'OUTPUT_TYPE'} eq 'string' ) {
my $ltext;
unless ($ltext = fill_in_file(
"$filepath",
HASH => $tmpl_vars,
BROKEN => $broken,
UNTAINT => 1,
PACKAGE => $pkg
))
{
carp "ERROR: Cannot process template $filepath: $Text::Template::ERROR\n";
return;
}
$text .= $ltext;
}
}
}
#################################################################
# Check for errors, and abort template processing if any occurred
#################################################################
if ($errorCount) {
if ( $p{'OUTPUT_TYPE'} eq 'file' ) {
close $fh;
unlink $tempfile;
}
my $msg = "Template processing failed for $outputfile:";
if ($warningCount) {
$msg .= " $warningCount fragment";
$msg .= "s" if $warningCount != 1;
$msg .= " generated warnings,";
}
$msg .= " $errorCount fragment";
$msg .= "s" if $errorCount != 1;
$msg .= " generated errors";
carp "ERROR: $msg\n";
return;
}
elsif ($warningCount) {
my $msg = "Template processing succeeded for $outputfile:";
$msg .= " $warningCount fragment";
$msg .= "s" if $warningCount != 1;
$msg .= " generated warnings";
carp "WARNING: $msg\n";
}
##############################################################
# Apply filters to the output, and do any necessary clean-up.
##############################################################
if ( $p{'OUTPUT_TYPE'} eq 'file' ) {
if ( defined $p{FILTER} ) {
_filter_fh( $fh, $ofh, $p{FILTER} );
}
# This should close the file descripter AND file handle
close $fh;
# make filename point to new inode
# NOTE: this is not an atomic operation, so on a non-journaling
# filesystem it is possible that the template could become corrupt
my $perms = $p{'PERMS'};
$perms = oct($perms) if $perms =~ /^0/;
# error checking and conversions for uid
my $uid = $p{'UID'};
if ( $uid =~ /^\d+$/ ) {
unless ( defined getpwuid $uid ) {
carp "WARNING: Invalid user: ${uid}, "
. "defaulting to 'root' user (0).\n";
$uid = 0;
}
}
else {
my $uname = $uid;
$uid = getpwnam $uid;
unless ( defined $uid ) {
carp "WARNING: Invalid user: ${uname}, "
. "defaulting to 'root' user (0).\n";
$uid = 0;
}
}
# error checking and conversions for gid
my $gid = $p{'GID'};
if ( $gid =~ /^\d+$/ ) {
unless ( defined getgrgid $gid ) {
carp "WARNING: Invalid group: ${gid}, "
. "defaulting to 'root' group (0).\n";
$gid = 0;
}
}
else {
my $gname = $gid;
$gid = getgrnam $gid;
unless ( defined $gid ) {
carp "WARNING: Invalid group: ${gname}, "
. "defaulting to 'root' group (0).\n";
$gid = 0;
}
}
# now do chown on our new target
chown( $uid, $gid, $tempfile )
|| carp "ERROR: Can't chown file $tempfile: $!\n";
# Now do chmod as well - POSIX::open does not change permissions
# of a preexisting file
chmod( $perms, $tempfile )
|| carp "ERROR: Can't chmod file $tempfile: $!\n";
unless ( -f $outputfile ) {
rename( "$tempfile", "$outputfile" )
or carp(
"ERROR: Could not rename $tempfile " . "to $outputfile: $!\n" );
return;
}
use Digest::MD5;
open( NEW, "$tempfile" );
my $newMD5sum = Digest::MD5->new->addfile(*NEW)->hexdigest;
close NEW;
open( OLD, "$outputfile" );
my $oldMD5sum = Digest::MD5->new->addfile(*OLD)->hexdigest;
close OLD;
if ( $oldMD5sum eq $newMD5sum ) {
$debug_template_expansion
&& warn("Not updating $outputfile - unchanged\n");
unlink "$tempfile";
# now do chown and chmod the file, to ensure permissions are correct
chown( $uid, $gid, $outputfile )
|| carp "ERROR: Can't chown file $tempfile: $!\n";
chmod( $perms, $outputfile )
|| carp "ERROR: Can't chmod file $tempfile: $!\n";
}
else {
$debug_template_expansion
&& warn(
"Updating $outputfile - MD5 was $oldMD5sum, now $newMD5sum\n");
rename( "$tempfile", "$outputfile" )
or carp(
"ERROR: Could not rename $tempfile " . "to $outputfile: $!\n" );
}
# copy any additional files
# A side effect of this routine is that it removes any old copies or
# proposed new copies that RPM leaves lying around. (i.e. F<.rpmsave>
# and F<.rpmnew> files.
-e "$outputfile.rpmsave" and unlink "$outputfile.rpmsave";
-e "$outputfile.rpmnew" and unlink "$outputfile.rpmnew";
}
elsif ( $p{'OUTPUT_TYPE'} eq 'string' ) {
if ( defined $p{FILTER} ) {
$text = _filter_text( $text, $p{FILTER} );
}
return $text;
}
}
=begin _private
=item _init_tmpl_vars
my $template_vars = _init_tmpl_vars(\%params);
Given the %params to processTemplate (after being adjusted for
defaults) it will generate a ref suitable for passing into
Text::Template->fill_in(HASH) to generate variables in the template.
=end _private
=begin testing
use esmith::TestUtils qw(scratch_copy);
my $scratch = scratch_copy('10e-smith-lib/configuration.conf');
$ENV{ESMITH_CONFIG_DB} = $scratch;
use esmith::ConfigDB;
my $db = esmith::ConfigDB->open;
my @recs = $db->get_all;
my $vars = esmith::templates::_init_tmpl_vars({});
is( keys %{$vars->[0]}, grep($_->props > 1, @recs),
' multi-prop ConfigDBs are hashes');
is( keys %{$vars->[1]}, grep($_->props <= 1, @recs),
' single-prop are scalars');
is( keys %{$vars->[2]}, 2, ' confref' );
isa_ok( $vars->[2]{DB}, 'REF', ' objects must be scalar refs for T::T' );
isa_ok( ${$vars->[2]{DB}}, 'esmith::ConfigDB' );
is( keys %{$vars->[3]}, 0, ' no MORE_DATA' );
$vars = esmith::templates::_init_tmpl_vars({ CONFREF => { foo => 42,
bar => 23,
} });
is( keys %{$vars->[0]}, grep($_->props > 1, @recs),
' multi-prop ConfigDBs are hashes');
is( keys %{$vars->[1]}, grep($_->props <= 1, @recs),
' single-prop are scalars');
is( keys %{$vars->[2]}, 2, ' confref' );
is_deeply( ${$vars->[2]{confref}}, { foo => 42, bar => 23 } );
is( keys %{$vars->[3]}, 2, ' MORE_DATA' );
is( $vars->[3]{foo}, 42 );
is( $vars->[3]{bar}, 23 );
$vars = esmith::templates::_init_tmpl_vars({ MORE_DATA => { foo => 42,
bar => 23,
} });
is( keys %{$vars->[0]}, grep($_->props > 1, @recs),
' multi-prop ConfigDBs are hashes');
is( keys %{$vars->[1]}, grep($_->props <= 1, @recs),
' single-prop are scalars');
is( keys %{$vars->[2]}, 2, ' confref' );
is( keys %{$vars->[3]}, 2, ' MORE_DATA' );
is( $vars->[3]{foo}, 42 );
is( $vars->[3]{bar}, 23 );
my $h_scratch = scratch_copy('10e-smith-lib/hosts.conf');
my $a_scratch = scratch_copy('10e-smith-lib/accounts.conf');
my $c_scratch = scratch_copy('10e-smith-lib/configuration.conf');
$ENV{ESMITH_CONFIG_DB} = $c_scratch;
$vars = esmith::templates::_init_tmpl_vars();
ok( ref ${ $vars->[2]{confref} } eq 'HASH', 'confref is HASH ref' );
=end testing
=cut
sub _init_tmpl_vars {
my ($p) = shift;
my @tmpl_vars = ();
# Start with the default set of ConfigDB vars
require esmith::ConfigDB;
my $conf_db = esmith::ConfigDB->open;
foreach my $rec ( $conf_db->get_all ) {
my $key = $rec->key;
my %props = $rec->props;
# Setup the hash
$tmpl_vars[0]{$key} = \%props if keys %props > 1;
# Setup the scalar
if (
$p->{ALL_RECORDS_AS_SCALARS}
|| ( keys %props <= 1
&& exists $props{type} )
)
{
$tmpl_vars[1]{$key} = $conf_db->{config}{$key};
}
}
# Add $confref and $DB
$tmpl_vars[2]{confref} =
$p->{CONFREF}
? \$p->{CONFREF}
: \$conf_db->{config};
$tmpl_vars[2]{DB} = \$conf_db;
# And any additional data
my $more_data = $p->{CONFREF} || $p->{MORE_DATA};
while ( my ( $var, $val ) = each %{$more_data} ) {
$tmpl_vars[3]{$var} = $val;
}
return \@tmpl_vars;
}
# for applying filters to an output filehandle
sub _filter_fh {
my ( $ifh, $ofh, $filter ) = @_;
# OK, we have a filter function to apply to the output
# So we rewind the anonymous output file, and read its contents
# then squirt it out into the named output file
$ifh->flush;
seek $ifh, 0, 0;
while (<$ifh>) {
print $ofh
join '', map { $filter->("$_\n") } split ( /\n/, $_ );
}
close $ifh;
$ofh->flush;
}
# for applying filters to a text string returns the filtered text
# string
sub _filter_text {
my ( $text, $filter ) = @_;
# We have a filter function to apply to the output text
return join '', map { $filter->("$_\n") } split ( /\n/, $text );
}
=begin testing
my %expect = (
'templates/template-begin' =>
'10e-smith-lib/templates/template-begin',
'templates/10moof' =>
'10e-smith-lib/templates/10moof',
'templates/template-end' =>
'10e-smith-lib/templates/template-end',
);
my %templates = esmith::templates::_merge_templates('templates',
'10e-smith-lib');
is_deeply( \%templates, \%expect, '_merge_templates' );
%expect = (
'templates2/template-begin' =>
'/etc/e-smith/templates-default/template-begin',
'templates2/10moof' =>
'10e-smith-lib/templates2/10moof',
'templates2/template-end' =>
'10e-smith-lib/templates2/template-end',
);
%templates = esmith::templates::_merge_templates('templates2',
'10e-smith-lib');
is_deeply( \%templates, \%expect, '_merge_templates() + template-begin' );
%templates = esmith::templates::_merge_templates('templates3',
'10e-smith-lib');
is( keys %templates, 0 );
# Bug 3110.
%templates = esmith::templates::_merge_templates('templates.t',
'10e-smith-lib');
%expect = (
'templates.t' => '10e-smith-lib/templates.t'
);
is_deeply( \%templates, \%expect, 'single file TEMPLATE_PATH' );
%templates = esmith::templates::_merge_templates('10moof',
'10e-smith-lib/templates2',
'10e-smith-lib/templates',
);
%expect = (
'10moof' => '10e-smith-lib/templates2/10moof'
);
is_deeply( \%templates, \%expect, 'single file TEMPLATE_PATH' );
=end testing
=cut
# the subroutine that does all the template merging
sub _merge_templates {
my %templates = ();
my $filename = shift;
my @template_queue = @_;
my $saw_dir = 0;
foreach my $source ( reverse @template_queue ) {
my $tmpl_path = "$source/$filename";
# if template is a flat template file overwrite the hash
if ( -f $tmpl_path ) {
%templates = ( $filename => $tmpl_path );
}
# otherwise, merge new fragments with the hash
elsif ( -d $tmpl_path ) {
$saw_dir = 1;
delete $templates{"$filename"};
# if dir exists but can't be opened then we have a problem
opendir( DIR, $tmpl_path )
|| carp "Can't open template source directory:"
. " $tmpl_path - skipping." && next;
# fill the hash with template fragments
while ( defined( my $file = readdir(DIR) ) ) {
next if ( $file =~ /^\.{1,2}$/ );
# Skip over files left over by rpm from upgrade
# and other temp files etc.
if ( $file =~ /(~|\.(swp|orig|rpmsave|rpmnew|rpmorig))$/o ) {
carp "Skipping $tmpl_path/$file";
next;
}
if ( -f "$tmpl_path/$file" ) {
# Untaint filename, else Text::Template will complain
$file =~ /(.*)/;
$templates{"$filename/$file"} = "$tmpl_path/$1";
}
elsif ( -d "$tmpl_path/$file" ) {
# silently ignore sub-directories
next;
}
}
closedir(DIR);
}
else {
next;
}
}
# If a directory template is active, and there is no
# template-default file, add a default one
if ( $saw_dir && keys %templates ) {
$templates{"${filename}/template-begin"} ||=
'/etc/e-smith/templates-default/template-begin';
}
return %templates;
}
=begin _private
=item I<_template_order>
my $cmp = _template_order;
Compares $a and $b returns -1, 0 or 1 if $template_file1 is less than,
equalto or greater than $template_file2.
Intended to be used as a sort function.
C<sort _template_order @templates>
Templates are ordered in ASCIIbetical order excepting that
template-begin always goes at the front and template-end at the end.
=end _private
=begin testing
use POSIX ':locale_h';
use locale;
setlocale(LC_ALL, "en_US");
$esmith::templates::a = '10Ahhh';
$esmith::templates::b = '10ahhh';
is( esmith::templates::_template_order(), -1 );
=end testing
=cut
# sort subroutine for use by 'sort' function to order template fragments
sub _template_order {
# so templates are always sorted ASCIIbetically, strictly speaking
# this is unnecessary as "use locale" is lexical.
no locale;
my $file_a = basename($a);
my $file_b = basename($b);
return -1 if $file_a eq "template-begin" || $file_b eq "template-end";
return 1 if $file_a eq "template-end" || $file_b eq "template-begin";
return $file_a cmp $file_b;
}
=head2 Filters
Filters are an experimental feature which allow you to filter the output
of a template in various ways.
Filtering functions take a single line at a time and return the
filtered version.
=over 4
=item removeBlankLines
Removes empty lines or those containing only whitespace from a
template.
=begin testing
use esmith::templates qw(removeBlankLines);
is( removeBlankLines(" "), '', 'removeBlankLines whitespace' );
is( removeBlankLines("\t"), '', ' tabs' );
is( removeBlankLines("\n"), '', ' newlines' );
is( removeBlankLines(""), '', ' empty' );
is( removeBlankLines(" a "), ' a ', ' not empty' );
=end testing
=cut
sub removeBlankLines {
$_[0] =~ /^\s*$/ ? '' : $_[0];
}
=head1 SEE ALSO
Section 3.4 "Templated Configuration System" of the E-Smith Dev Guide
=head1 AUTHOR
Mitel Networks Corporation
For more information, see http://www.e-smith.org/
=cut
1;