1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
|
#!/usr/bin/perl
use strict;
use warnings;
# This script uses all the information available in LCDd.conf to create a model
# for LCDd configuration file
# How does this work ?
# The conventions used in LCDd.conf template file are written in a way
# which makes it relatively easy to parse to get all required
# information to build a model.
# All drivers are listed, most parameters have default values and
# legal values written in comments in a uniform way. Hence this file
# (and comments) can be parsed to retrieve the information required to
# create a consistent model for LcdProc configuration. Some useful
# parameters are commented out in LCD.conf. So some processing is
# required to be able to create a model with these commented
# parameters. See below for this processing.
# This script performs the following tasks:
# 1/ check whether generating the model is necessary (or possible)
# 2/ pre-process LCDd.conf template
# 3/ parse the new LCDd.conf template
# 4/ mine the information there and translate them in a format suitable to create
# a model. Comments are used to provide default and legal values and also to provide
# user documentation
# 5/ Write the resulting LCDd model
use Config::Model 2.141;
use Config::Model::Itself 2.022; # to create the model
use 5.010;
use Path::Tiny;
use Getopt::Long;
my $verbose = 0;
my $show_model = 0;
my $force = 0;
my $source = "lcdproc/LCDd.conf" ;
my $result = GetOptions (
"verbose" => \$verbose,
"model" => \$show_model,
"force" => \$force,
"file=s" => \$source,
);
die "Unknown option. Expected -verbose, -force, -file or -model" unless $result ;
########################
#
# Step 1: Check whether generating lcdproc model is necessary.
my $target = "lib/Config/Model/models/LCDd.pl";
my $script = "script/lcdconf2model.pl";
if (-e $target and -M $target < -M $script and -M $target < -M $source) {
say "LcdProc model is up to date";
exit unless $force;
}
say "Building lcdproc model from upstream LCDd.conf file $source" ;
###########################
#
# Step 2: pre-process LCDd.conf (INI file)
# Here's the LCDd.conf pre-processing mentioned above
# read LCDd.conf
my @lines = path($source)->lines;
# un-comment commented parameters and put value as default value
foreach my $line (@lines) {
$line =~ s/^#(\w+)=(.*)/# [default: $2]\n$1=$2/;
}
# write pre-processed files
my $path = path('.');
my $tmp = $path->child('tmp');
$tmp->mkpath;
$tmp->child('LCDd.conf')->spew(@lines);
###########################
#
# Step 3: parse LCDd.conf (INI file)
# Problem: comments must also be retrieved and associated with INI
# class and parameters
# Fortunately, Config::Model::Backend::IniFile can already perform this
# task.
# On the other hand, Config::Model::Backend::IniFile must store its
# values in a configuration tree. A model suitable for LCDd.conf that
# accepts any INI class and any INI parameter must be created
# Dump stack trace in case of error
Config::Model::Exception::Any->Trace(1) ;
# one model to rule them all
my $model = Config::Model->new();
# The model for pre-precessed LCDd.conf must be made of 2 classes:
# - the main config class that contains INI class names (named Dummy here)
# - the child class that contains data from a elements of the INI
# classes (named Dummy::Class)
# For techinical reason, the lower class (Dummy::Class) must be
# created first.
# The class is used to store any parameter found in an INI class
$model->create_config_class(
name => 'Dummy::Class',
accept => [
'Hello|GoodBye|key' => {
type => 'list',
cargo => { qw/type leaf value_type uniline/}
},
'.*' => {
type => 'leaf',
value_type => 'uniline'
}
],
);
# This class contains any INI class, and use Dummy::Class to hold parameters.
$model->create_config_class(
name => 'Dummy',
accept => [
'.*' => {
type => 'node',
config_class_name => 'Dummy::Class'
}
],
rw_config => {
backend => 'IniFile',
config_dir => 'tmp', # created above
file => 'LCDd.conf'
}
);
# Now the dummy configuration class is created. Let's create a
# configuration tree to store the data from LCDd.conf
my $dummy = $model->instance(
instance_name => 'dummy',
root_class_name => 'Dummy',
)-> config_root;
##############################################
#
# Step 4: Mine the LCDd.conf information and create a model
#
# Create a meta tree that will contain LCDd model
my $meta_root = $model->instance(
root_class_name => 'Itself::Model',
instance_name => 'meta_model',
) -> config_root;
# Create LCDd configuration class and store the first comment from LCDd.conf as
# class description
$meta_root->grab("class:LCDd class_description")->store( $dummy->annotation );
# append my own text
my $extra_description = "Model information was extracted from /etc/LCDd.conf";
$meta_root->load(qq!class:LCDd class_description.="\n\n$extra_description"!);
# add legal stuff
$meta_root->load( qq!
class:LCDd
copyright:0="2011-2017, Dominique Dumont"
copyright:1="1999-2017, William Ferrell and others"
license="GPL-2"
!
);
# add INI backend (So LCDd model will be able to read INI files)
$meta_root->load( qq!
class:LCDd
rw_config
backend=ini_file
config_dir="/etc"
file="LCDd.conf"
quote_value=shell_style
!
);
# Note: all the load calls above could be done in one call. They are
# split in several class to clarify what's going on.
# Now, let's use the information retrieved by /etc/LCDd.conf
# and stored in Dummy tree.
# @ini_classes array contains all INI classes found in LCDd.conf,
# make sure to put server in first, and sort the rest
my @ini_classes = sort grep { $_ ne 'server'} $dummy->get_element_name;
unshift @ini_classes, 'server' ;
# Now before actually mining LCDd.conf information, we must prepare
# subs to handle them. This is done using a dispatch table.
my %dispatch;
# first create the default case 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
$dispatch{_default_} = sub {
my ( $ini_class, $ini_param, $info_r, $ini_v, $value_type ) = @_;
# prepare a string to create the ini_class model
my $load = qq!class:"$ini_class" element:$ini_param type=leaf !;
$value_type ||= 'uniline';
# get semantic information from comment (written between square brackets)
my $square_model = '';
my $square_rexp = '\[(\s*\w+\s*:[^\]]*)\]';
if ($$info_r =~ /$square_rexp/s) {
my $info = $1 ;
say "class $ini_class element $ini_param info: '$info'" if $verbose;
$$info_r =~ s/$square_rexp//gs; # remove all remaining square_rexp
$square_model .= ' '. info_to_model($info,$value_type, $info_r) ;
}
unless ($square_model) {
# or use the value found in INI file as default
$ini_v =~ s/^"//g;
$ini_v =~ s/"$//g;
$square_model .= qq! value_type=$value_type!;
$square_model .= qq! default="$ini_v"! if length($ini_v);
}
# get model information from comment (written between curly brackets)
my $curly_model = '';
my $curly_rexp = '{%(\s*\w+.*?)%}' ;
while ($$info_r =~ /$curly_rexp/s) {
$curly_model = $1 ;
say "class $ini_class element $ini_param model snippet: '$curly_model'"
if $verbose;
$$info_r =~ s/$curly_rexp//s;
}
# return a string containing model specifications
# spec in curly model may override spec in square model
return $load . $square_model . $curly_model ;
};
# Now let's take care of the special cases. This one deals with "Driver"
# parameter found in INI [server] class
$dispatch{"LCDd::server"}{Driver} = sub {
my ( $class, $elt, $info_r, $ini_v ) = @_;
my $load = qq!class:"$class" element:$elt type=check_list !;
my @drivers = split /\W+/, $$info_r;
while ( @drivers and ( shift @drivers ) !~ /supported/ ) { }
$load .= 'choice=' . join( ',', @drivers ) . ' ';
#say $load; exit;
return $load;
};
# Ensure that DriverPath ends with a slash by adding a match clause
$dispatch{"LCDd::server"}{DriverPath} = sub {
return $dispatch{_default_}->( @_ ) . q! match="/$"! ;
};
# like default but ensure that the parameter is integer
$dispatch{"LCDd::server"}{WaitTime}
= $dispatch{"LCDd::server"}{ReportLevel}
= $dispatch{"LCDd::picolcd"}{LircFlushThreshold}
= $dispatch{"LCDd::server"}{Port}
= sub {
my ( $class, $elt, $info_r, $ini_v ) = @_;
return $dispatch{_default_}->( @_, 'integer' );
};
# special dispatch case
my %override ;
# Handle display content
$override{"LCDd::server"}{GoodBye}
= $override{"LCDd::server"}{Hello}
= $override{"LCDd::linux_input"}{key}
= sub {
my ( $class, $elt ) = @_;
my $ret = qq( class:"$class" element:$elt type=list ) ;
$ret .= 'cargo type=leaf value_type=uniline';
return $ret ;
};
# Now really mine LCDd.conf information using Dummy tree
# loop over all INI classes
foreach my $ini_class (@ini_classes) {
say "Handling INI class $ini_class" if $verbose;
my $ini_obj = $dummy->grab($ini_class);
my $config_class = "LCDd::$ini_class";
# create config class in case there's no parameter in INI file
$meta_root->load(qq!class:"LCDd::$ini_class" class_description="generated from LCDd.conf"!);
# loop over all INI parameters and create LCDd::$ini_class elements
foreach my $ini_param ( $ini_obj->get_element_name ) {
my ($model_spec) ;
# test for override
if (my $sub = $override{$config_class}{$ini_param}) {
# runs the override sub to get the model string
$model_spec = $sub->($config_class, $ini_param) ;
}
else {
# retrieve the correct sub from the orveride or dispatch table
my $sub = $dispatch{$config_class}{$ini_param} || $dispatch{_default_};
# retrieve INI value
my $ini_v = $ini_obj->grab_value($ini_param);
# retrieve INI comment attached to $ini_param
my $ini_comment = $ini_obj->grab($ini_param)->annotation;
# runs the sub to get the model string
$model_spec = $sub->($config_class, $ini_param, \$ini_comment, $ini_v) ;
# escape embedded quotes
$ini_comment =~ s/"/\\"/g;
$ini_comment =~ s/\n*$//;
$model_spec .= qq! description="$ini_comment"! if length($ini_comment);
}
# show the model without the doc (too verbose)
say "load -> $model_spec" if $show_model ;
# load class specification in model
$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
! ;
if ( $ini_class eq 'server' or $ini_class eq 'menu' ) {
$driver_class_spec .= qq!
type=node
config_class_name="LCDd::$ini_class"
! ;
}
else {
# Arrange a driver class is shown only if the driver was selected
# in the [server] class
$driver_class_spec .= qq!
type=warped_node
config_class_name="LCDd::$ini_class"
level=hidden
warp
follow:selected="- server Driver"
rules:"\$selected.is_set('$ini_class')"
level=normal
!;
}
$meta_root->load($driver_class_spec);
}
######################
#
# Step 5: write the model
# Itself constructor returns an object to read or write the data
# structure containing the model to be edited. force_write is required
# because writer object, being created *after* loading the model in the
# instance, is not aware of these changes.
my $rw_obj = Config::Model::Itself->new(
model_object => $meta_root,
cm_lib_dir => 'lib/Config/Model/',
force_write => 1,
);
say "Writing all models in file (please wait)";
$rw_obj->write_all;
# mop up
$tmp->remove_tree;
say "Done";
# this function extracts info specified between square brackets and returns a model snippet
sub info_to_model {
my ($info,$value_type, $info_r) = @_ ;
$info =~ s/\s+//g;
my @model ;
# legal needs to be parsed first to setup value_type first
my %info = map { split /[:=]/,$_ ,2 ; } split /;/,$info ;
# use this semantic information to better specify the parameter
if (my $legal = delete $info{legal} || '') {
if ( $legal =~ /^([\d.]*)-([\d.]*)$/ or $legal =~ /^>([\d.]+)$/ ) {
my $bounds = '';
$bounds.= "min=$1 " if defined $1 and length($1);
$bounds.= "max=$2 " if defined $2 and length($2);
my $vt = "value_type=";
$vt .= $bounds =~ m/\./ ? 'number ' : 'integer ';
push @model, $vt.$bounds;
}
elsif ($legal =~ /^(on,off|off,on)$/ ) {
push @model, "value_type=boolean write_as=off,on"
}
elsif ($legal =~ /^(yes,no|no,yes)$/ ) {
push @model, "value_type=boolean write_as=no,yes"
}
elsif ($legal =~ /^([\w\,]+)$/ ) {
push @model, "value_type=enum choice=$1"
}
else{
# push back $legal info if no model snippet could be extracted
say "note: unhandled legal spec: '$legal'. Sending it back to doc";
push @model, "value_type=$value_type ";
$$info_r .= "legal: $legal "
}
}
else {
push @model, "value_type=$value_type ";
} ;
foreach my $k (keys %info) {
my $v = $info{$k} ;
die "Undefined value. Something is wrong in info '$info'" unless defined $v ;
$v = '"'.$v.'"' unless $v=~/^"/ ;
if ($k =~ /default/ ) {
# specify upstream default value if it was found in the comment
push @model ,qq!upstream_default=$v! if length($v);
}
elsif ($k =~ /assert/ ) {
push @model ,qq!warn_unless:0 code=$v -!;
}
else {
push @model, "$k=$v" ;
}
}
return join(' ',@model) ;
}
|