Dominique Dumont: Generate a configuration editor from a config template file with Perl: lcdproc example
Hello
Creating a configuration model for Config::Model can be a boring task: When a configuration system has a lot of parameters, a lot of details need to be specified to have a complete configuration validation tool. So, all tricks are permitted to make this task more bearable.
Let s see how this can be applied to lcdproc. This project requires a configuration file that supports more than 40 drivers. Each driver has it own set of parameters for a total of about 250 parameters. That s a lot to specify..
Fortunately, LCDd.conf template file is written in a way which makes it relatively easy to parse and get all required information to build a model.
All drivers are listed in this template file, most parameters have default values and legal values are written in comments in a uniform way. Hence this file (and comments) can be parsed to retrieve information required for the model.
This blog will describe how this task can be implemented in Perl using Config::Model modules. The code will be slightly trimmed down to keep this blog (relatively) short. All initialisation code and error checks are removed. You can get the whole script on sourceforge.
The main steps are:
Problem: LCDd.conf comments must also be analysed to get more semantic information. The comments must be retrieved and associated with the INI classes and parameters found in this file. Fortunately, Config::Model::Backend::IniFile is able to associate comments with the relevant parameters. But Config::Model::Backend::IniFile must store its values in a configuration tree. So let s create a model suitable for LCDd.conf template that accepts any INI class and any INI parameter Dummy::Class is used to store any parameter found in an INI class. The accept keyword with .* regular expression means that any INI parameter will be accepted in each INI class.
- prepare a parser for LCDd.conf template
- parse LCDd.conf template
- prepare a LCDd model skeleton
- translate LCDd.conf template information in a format suitable to create a model.
- Write the resulting LCDd model
use strict; use warnings;Then load all required libraries. Their use will be described later:
use Config::Model; use Config::Model::Itself 1.225; use Config::Model::Backend::IniFile; use 5.10.0; use IO::File; use IO::String;Perform some initialisation to avoid upsetting the logging system:
use Log::Log4perl qw(:easy); Log::Log4perl->easy_init($WARN);Let s also initialize a model object that will be very useful later:
my $model = Config::Model->new();Now we can really start the fun part. Step 1: Create a parser for LCDd.conf (INI file)
Problem: LCDd.conf comments must also be analysed to get more semantic information. The comments must be retrieved and associated with the INI classes and parameters found in this file. Fortunately, Config::Model::Backend::IniFile is able to associate comments with the relevant parameters. But Config::Model::Backend::IniFile must store its values in a configuration tree. So let s create a model suitable for LCDd.conf template that accepts any INI class and any INI parameter Dummy::Class is used to store any parameter found in an INI class. The accept keyword with .* regular expression means that any INI parameter will be accepted in each INI class.
$model->create_config_class(
name => 'Dummy::Class',
accept => [
'.*' =>
type => 'leaf',
value_type => 'uniline'
,
],
);
Now the Dummy model must provide a configuration class to hold all INI classes:
$model->create_config_class(
name => 'Dummy',
accept => [
'.*' =>
type => 'node',
config_class_name => 'Dummy::Class'
,
],
);
Note that an INI backend could be specified directly here. But, some useful parameters are commented out in LCD.conf. Some some processing is required to be able to create a model with these commented parameters. See below for this processing.
Step 2: Parse LCDd.conf
Now the dummy configuration class is created. Let s create a configuration tree from this dummy class to store the data from LCDd.conf
my $dummy = $model->instance(
instance_name => 'dummy',
root_class_name => 'Dummy',
)-> config_root;
Now, read LCDd.conf:
my $lcd_file
= IO::File->new('examples/lcdproc/LCDd.conf');
my @lines = $lcd_file->getlines;
And perform the pre-processing on LCDd.conf mentioned above. Just un-commenting commented parameters is required:
foreach (@lines) s/^#(\w+=)/$1/Store the pre-processed LCDd.conf in a IO::Handle usable by INI backend
my $ioh = IO::String->new( join( '', @lines ) );Create an INI backend to read the data from pre-processed LCDd.conf and store it in the configuration tree:
my $ini_backend = Config::Model::Backend::IniFile ->new( node => $dummy ); $ini_backend->read( io_handle => $ioh );Step 3: Prepare LCDd configuration model Your attention please. You may remember that we invoked instance on object to create the dummy configuration tree to hold LCDd.conf data. The instance method used the Dummy configuration class. Now, we must invoke instance again on object. But this time, instance will use Config::Model::Itself. This model is Config::Model s way of eating its own dog food. The configuration tree created there will be able to hold LCDd model. Hence the root of this special configuration tree will be called meta_root to avoid confusion with the other configuration root defined above (for the Dummy model). Still with me? Good, let s create this meta tree that will contain LCDd model:
my $meta_root = $model->instance(
root_class_name => 'Itself::Model',
instance_name => 'meta_model',
) -> config_root;
Create the main LCDd configuration class and store the first comment from LCDd.conf as class description:
my $first_comment = $dummy->annotation ;
$meta_root
->grab("class:LCDd class_description")
->store( $first_comment );
Append my own text in this description:
my $extra_description =
"Model information extracted from template /etc/LCDd.conf"
. "\n\n=head1 BUGS\n\nThis model does not support to load "
. "several drivers. Loading several drivers is probably a "
. "marginal case. Please complain to the author if this "
. "assumption is false";
$meta_root->load(
qq!class:LCDd
class_description.="\n\n$extra_description"!
);
Add legal stuff in the model:
$meta_root->load( qq!
class:LCDd
copyright:0="2011, Dominique Dumont"
copyright:1="1999-2011, William Ferrell and others"
license="GPL-2"
!
);
Add INI backend so configuration object created from LCDd model will be able to read user s INI files:
$meta_root->load( qq!
class:LCDd
read_config:0
backend=ini_file
config_dir="/etc"
file="LCDd.conf"
!
);
Note that all the load calls above could be done as one call. They were split for better clarity.
Step 4:Extracting LCDd.conf information
Now that all information from LCDd.conf template file is stored in $dummy configuration tree, it s time to use this information to enhance the skeleton of LCDd model created above.
Let s first find all the INI classes defined in LCDd.conf:
my @ini_classes = $dummy->get_element_name;Now before actually mining LCDd.conf information, we must prepare several subs to handle them. This is done using a dispatch table:
my %dispatch;First create the default entry which will be used for most parameters. This subs is passed: the INI class name, the INI parameter name, the comment attached to the parameter, the INI value, and an optional value type. It will return a string describing a model for simple elements. For instance: class:"LCDd::MD8800" element:Device type=leaf value_type=uniline default="/dev/ttyS1" description="device to use [default: /dev/ttyS1]" Here s the subroutine invoked by default. Let s create a an anonymous subroutine and store it in the dispatch table:
$dispatch _default_ = sub
my ( $ini_class, $ini_param, $ini_note,
$ini_v, $value_type ) = @_;
$value_type = 'uniline';
Now, prepare a string that will be used later to create the ini_class model. This string uses Config::Model s serialisation format described in Config::Model::Loader:
my $load = qq!class:"$ini_class"
element:$ini_param type=leaf !;
Get semantic information from LCDd.conf template comments (written between square brackets):
my $info = $ini_note =~ /\[(.*)\]/ ? $1 : '';
$info =~ s/\s+//g;
Use this semantic information to better specify the parameter:
$load .=
$info =~ /legal:(\d+)-(\d+)/
? " value_type=integer min=$1 max=$2"
: $info =~ /legal:([\w\,]+)/
? " value_type=enum choice=$1"
: " value_type=$value_type";
if ( $info =~ /default:(\w+)/ )
# specify upstream default value
# if it was found in the comment
$load .= qq! upstream_default="$1"!
if length($1);
else
# or use the value found in INI
# file as default
$ini_v =~ s/^"//g;
$ini_v =~ s/"$//g;
$load .= qq! default="$ini_v"!
if length($ini_v);
The model (in serialised format) is ready, let s return it and exit the subroutine:
return $load; ;Now let s take care of the special cases. This one deals with Driver parameter found in INI [server] class. This sub will create a string for an enum element. The possible values of the enum are extracted from the comment found in the LCDd.conf file:
$dispatch "LCDd::server" Driver = sub
my ( $class, $elt, $info, $ini_v ) = @_;
my $load = qq!class:"$class"
element:$elt
type=leaf
value_type=enum
!;
my @drivers = split /\W+/, $info;
while ( @drivers
and ( shift @drivers ) !~ /supported/ )
# nothing, the work is done by shift operator
# in loop test to skip all words in $info until
# 'The following drivers are supported:'.
# ok, that's a hack
$load .= 'choice='
. join( ',', @drivers ) . ' ';
return $load;
;
This sub invokes the default sub and add a small feature to the model to ensure that DriverPath ends with / . This is ensured by the match="/$" snippet:
$dispatch "LCDd::server" DriverPath = sub
my ( $class, $elt, $info, $ini_v ) = @_;
return $dispatch _default_ ->(@_)
. q! match="/$" !;
;
Likewise, this sub invokes the default sub with the optional 4th parameter that forces the value_type of the element. In this case, we enforce an integer element:
$dispatch "LCDd::server" WaitTime
= $dispatch "LCDd::server" ReportLevel
= $dispatch "LCDd::server" Port
= sub
my ( $class, $elt, $info, $ini_v ) = @_;
return $dispatch _default_ ->( @_, 'integer' );
;
This sub returns a string for a list element with default values:
$dispatch "LCDd::server" GoodBye
= $dispatch "LCDd::server" Hello
= sub
my ( $class, $elt, $info, $ini_v ) = @_;
my $ret = qq( class:"$class"
element:$elt
type=list
) ;
$ret .= 'cargo type=leaf
value_type=uniline - ' ;
$ret .= 'default_with_init:0="\" '.$elt.'\"" ' ;
$ret .= 'default_with_init:1="\" LCDproc!\""';
return $ret ;
;
Now that the dispatch table is ready, let s really mine LCDd.conf information.
First handle all INI classes in the foreach loop:
foreach my $ini_class (@ini_classes)Let s retrieve parameters for this INI class from the LCDd.conf template:
my $ini_obj = $dummy->grab($ini_class);This will be the name of the lcdproc driver configuration class (e.g. LCDd::imonlcd):
my $config_class = "LCDd::$ini_class";The configuration class will also be created in a serialised format. Let s create config class in case there s no parameter in INI file:
$meta_root->load(qq!class:"LCDd::$ini_class"!);Now, loop over all INI parameters and create LCDd::$ini_class elements:
my @all_ini_params = $ini_obj->get_element_name ; foreach my $ini_param ( @all_ini_params )Retrieve the value provided in template LCDd.conf:
my $ini_v = $ini_obj->grab_value($ini_param);Retrieve the comment attached to the value provided in template LCDd.conf:
my $ini_comment
= $ini_obj->grab($ini_param)->annotation;
# escape embedded quotes
$ini_comment =~ s/"/\\"/g;
Retrieve the correct sub from dispatch table:
my $sub = $dispatch $config_class $ini_param
$dispatch _default_ ;
Run the sub to get the model string:
my $model_spec
= $sub->( $config_class, $ini_param,
$ini_comment, $ini_v );
Attach the comment from LCDd.conf as class description:
$model_spec .= qq! description="$ini_comment"!
if length($ini_comment);
Then, last but not least, load class specification in the model, and end the loop aroung INI parameters:
$meta_root->load($model_spec);Now create a an $ini_class element in LCDd class (to link LCDd class and LCDd::$ini_class):
my $driver_class_spec = qq!
class:LCDd
element:$ini_class
! ;
Since server and menu are not drivers, they have a special treatment:
if ( $ini_class eq 'server'
or $ini_class eq 'menu' )
$driver_class_spec .= qq!
type=node
config_class_name="LCDd::$ini_class"
! ;
Here s the treatment for LCDd drivers. Setup the model with a warped node so that a driver configuration class is shown only if the driver was selected in the [server] class:
else
$driver_class_spec .= qq!
type=warped_node
config_class_name="LCDd::$ini_class"
level=hidden
follow:selected="- server Driver"
rules:"\$selected eq '$ini_class'"
level=normal
!;
Now we can load the LCDd driver model in meta root:
$meta_root->load($driver_class_spec);Step 5: save LCDd model Still with me ? Good. Here s the last part, now that the model was created in memory, let s save it in files. Config::Model::Itself constructor returns an object to read or write the data:
my $rw_obj = Config::Model::Itself
->new( model_object => $meta_root );
say "Writing all models in file (please wait)";
$rw_obj->write_all(
model_dir => 'lib/Config/Model/models/'
);
That s it. Once this script is run, the directory lib/Config/Model/models/ contains a bunch of files:
- The main LCDd model
- All models for the drivers
- command lines to validate configuration
- graphical configuration editor with integrated help
- Configuration doc generated from the information stored in the template file



I recently uploaded new upstream versions of two neat small X tools to
Long time no see Not much hacking in the last few months. Not much ranting either (some of you I m sure will appreciate ;-).
Anyway, I recently grew excited to learn that
Again, there's been this surreal time warp effect. 3 months has passed, and it
feels like both a long time and a blink of an eye. It more feels like a long
time though. I've mostly forgotten what it was like before Zoe came along.
Zoe is growing well. She weighs around 11 lbs 11 oz (5.3 kg), and I swear she's
getting very long. When I'm holding her in my arms, there's legs everywhere.
She started being able to roll from her stomach onto her back at 2 months, and
her neck strength is pretty good now. I guess her next significant milestone
will be being able to sit up properly. When she's on her tummy, she can prop
herself up on her elbows and look around. She smiles all the time.
We had a few nights of 8 hours of sleep, then everything went completely out
the window, and we had about a week of very disrupted sleep at night. I think
she's starting to settle into a routine now though, with a few 5 hour stints
the last few nights.
Her US passport arrived yesterday. We still need to get everything together to
lodge her Australian citizenship application, which is the first step towards
getting her an Australian passport.
Sarah's back studying now (two subjects this semester), which is proving to be
a bit of a juggling act.
Async Map and Fold in JavaScript
My latest experiment is an implementation of
asynchronous/parallel "map" (and other array functions) in
JavaScript
and 

Originally I planned to post this with the title "Last night I slept with 300 people" but this night happened two weeks ago... It was the night before the fourth and last day of the