line
stmt
bran
cond
sub
pod
time
code
1
#!/usr/bin/perl
2
3
package Config::Cascade;
4
5
1
1
24331
use warnings;
1
2
1
65
6
1
1
6
use strict;
1
2
1
35
7
8
1
1
874
use Regexp::Common;
1
5657
1
5
9
10
my %Options;
11
my %configValidation;
12
13
=head1 NAME
14
15
Config::Cascade - simple configuration file framework for managing multi-level configurations, with regexp validation.
16
17
=head1 VERSION
18
19
Version 0.02
20
21
=cut
22
23
our $VERSION = '0.02';
24
25
=head1 SYNOPSIS
26
27
Config::Cascade is intended to allow the use of global configurations in combination with more specific
28
configs, with the added benefit of overriding all of these settings from the command line. This benefit
29
allows the use of a standard base config, with a simple format, while allowing custom configs for multiple
30
programs utilizing the same resources or configurations. Validated configuration options will function in both global
31
and specific configuration files, and may also be referenced from the command line using Getopt::Long and getopt style
32
notation:
33
--= or -=
34
35
Example:
36
37
use Config::Cascade;
38
my %config = Config::Cascade->new(
39
configDir => '/etc/Frood',
40
globalConfig => 'global.cfg',
41
configFile => 'example.cfg',
42
validate => {
43
host => {type => 'fqdn'},
44
port => {type => 'regexp', args=> 'RE::num::int'},
45
url => {type => 'regexp', args => '^http:'},
46
},
47
);
48
49
print $config{host};
50
51
=head1 Validation File format
52
53
A validation file follows a simple format, one entry per line:
54
55
56
57
Config variable names are arbitrary, but must be single strings, sans white space. The following is a list of valid data types.
58
59
alias - Declares this entry to be an alias for another. Alias requires an optional argument referring to the parent option.
60
61
bool - Sets the value to '1' if present.
62
63
int - Matches the equiv of $RE{num}{int}
64
65
fqdn - Matches fully qualified domain names for formatting, using $RE{net}{domain}
66
67
regexp - Matches a free form regular expression, or refers to an entry in Regexp::Common. This requires an optional argument of a valid regular expression, or RE reference. Specifying 'RE::' informs the parser a Regexp::Common regexp is being invoked, with subsequent delimited entries corresponding to Regexp::Common's multi-level hash syntax. Otherwise, the contents of args will be precompiled as-is and matched accordingly.
68
69
string - Matches the equiv of /\w+/
70
71
=head1 Config File format
72
73
A configuration file follows a simple format, one entry per line:
74
75
76
=head1 FUNCTIONS
77
78
=head2 new
79
80
Performs start-up sanity checks and functions. The module will at first attempt to load a
81
global configuration file, if available. After that, a more specific configuration file (if available),
82
is loaded, overriding any settings in the global configuration that collide. After that, command line
83
options then override any loaded settings that collide.
84
85
Valid options:
86
configDir - Specifies a directory containing configuration files.
87
- If not specified, will use the current working directory.
88
configFile - Specifies a specific configuration file to be read.
89
- If not specifed, will be ignored.
90
debug - enables debug output.
91
globalConfig - Specifies a global config shared between multiple programs.
92
- If not specified, global.cfg will be looked for in the specified configDir.
93
noCommandLine - Skips parsing of @ARGV.
94
noConfig - Skips reading of configuration files. Command line options will be used.
95
noValidation - Skips use of validation functions entirely.
96
validate - Optional hash structure containing validation instructions.
97
validationFile - Specifies a file containing configuration validation instructions.
98
- If not specified, global.validation will be looked for in the specified configDir.
99
100
=cut
101
102
sub new {
103
0
0
1
my ($class, $hashref) = @_;
104
105
0
%Options = %{$hashref};
0
106
0
my %running; # The current config hash, as it passes from phase to phase.
107
my %global; # Application specific config
108
0
my %specific; # Application specific config
109
0
my %commandLine; # Command line options
110
111
0
0
$Options{configDir} or $Options{configDir} = '.'; # No configDir? Use pwd.
112
113
0
0
0
$Options{globalConfigFile} = 'global.cfg' unless ($Options{globalConfigFile} || $Options{noConfig});
114
115
# Build validation base in preparation for reading and validation of config files and command line
116
0
0
unless($Options{noValidation}) {
117
0
0
if($Options{validation}) { %configValidation = %{ $Options{validation} }}
0
0
118
0
else { %configValidation = loadDefaultValidation() }
119
120
0
0
constructValidation() or die "Errors found in validation structure, aborting.\n";
121
}
122
123
0
0
unless($Options{noConfig}) {
124
0
0
die "Specified configDir ($Options{configDir}) doesn't appear to be a directory.\n" unless (-d $Options{configDir});
125
0
0
die "Specified configFile doesn't appear to exist." unless ( -e "$Options{configDir}/$Options{configFile}");
126
127
# Global config has no failure check, because it's optional.
128
129
# Read global options
130
0
%global = loadConfigFile($Options{configDir} . '/' . $Options{globalConfigFile} );
131
0
0
unless ($Options{noValidation}) {
132
0
0
validateConfig(%global) or die "Global config failed validation.";
133
}
134
135
# Read process specific options
136
0
%specific = loadConfigFile($Options{configDir} . '/' . $Options{configFile});
137
0
0
unless ($Options{noValidation}) {
138
0
0
validateConfig(%specific) or die "Config failed validation.";
139
}
140
141
0
%running = %global;
142
143
}
144
145
0
0
unless($Options{noCommandLine}) {
146
0
my %commandLine = parseCommandLine(@main::ARGV);
147
0
0
unless ($Options{noValidation}) {
148
0
0
validateConfig(%specific) or die "Config failed validation.";
149
}
150
151
# Override with command line options
152
0
0
if($Options{debug}) {
153
0
foreach my $key (sort keys %commandLine) { warn "Command Line: $key: $commandLine{$key}\n"; }
0
154
}
155
156
}
157
158
0
foreach my $key ( keys %specific ) {
159
0
0
0
if(exists $global{$key} && $Options{debug}) {
160
0
warn "Specific option $key overriding global value ($running{$key}) with:$specific{$key}\n";
161
}
162
163
0
$running{$key} = $specific{$key};
164
}
165
166
0
foreach my $key ( keys %commandLine ) {
167
0
0
0
if($global{$key} && $Options{debug}) {
168
0
warn "Specific option $key overriding global value ($running{$key}) with:$commandLine{$key}\n";
169
}
170
171
0
$running{$key} = $commandLine{$key};
172
}
173
174
0
return(%running);
175
}
176
177
sub loadConfigFile {
178
0
0
0
my $target = shift;
179
0
my %hash; local *IN;
0
180
181
0
0
open(IN, $target) or die "Error opening config file ($target): $!\n";
182
0
while() {
183
0
chomp;
184
0
my $line = $_; $line =~ s/^\s+//;
0
185
0
0
next if $line eq '';
186
0
my ($command, $opt) = $line =~ /^(\w+)\s*(.*)/;
187
188
# Check and expand aliases
189
0
0
0
if(!$Options{noValidation} && $configValidation{$command}{type} eq 'alias') {
190
0
$command = $configValidation{$command}{arg};
191
192
0
0
0
if($hash{$command} && $hash{$command} ne $opt) { # Alias expansion collision
193
0
warn "Alias expansion for $command has resulted in a collision, skipping alias\n";
194
}
195
}
196
0
else { $hash{$command} = $opt; }
197
198
0
0
warn "Config($target): $command = $opt\n" if $Options{debug};
199
}
200
0
close(IN);
201
0
return %hash;
202
}
203
204
sub loadDefaultValidation {
205
0
0
0
my($dir, $target);
206
207
0
$dir = $Options{configDir};
208
0
0
$target = $Options{validationFile} or $target = 'global.validation'; # Set default if needed
209
210
0
0
return(0) unless ( -e "$dir/$target" ); # Fail quietly if it doesn't exist, as it may not be in use
211
212
0
my %hash; local *IN;
0
213
214
0
0
if(open(IN, "$dir/$target")) {
215
0
my $count = 0;
216
217
0
while() {
218
0
$count++;
219
0
chomp;
220
0
my $line = $_;
221
222
0
$line =~ s/(.*)\#.*$/$1/; # Strip anything resembling a comment because that's only used by the humans
223
0
0
next if $line eq ''; # Skip the line if we just reduced it to nothing
224
225
0
0
if( my ($option, $format, $arg) = $line =~ /^(\w+)\s+(string|int|alias|fqdn|bool|regexp)\s*(.*)/i ) {
226
# Check for valid format
227
0
$hash{$option}{type} = $format;
228
0
$hash{$option}{arg} = $arg;
229
}
230
else {
231
0
warn "$dir/$target: Invalid format on line $count, ignoring: $line\n";
232
}
233
}
234
0
close(IN);
235
0
return(%hash);
236
}
237
else {
238
0
warn "Unable to read $dir/$target: $!\n";
239
0
return(0);
240
}
241
}
242
243
sub constructValidation {
244
245
0
0
0
my $regexp;
246
247
# Run through and expand Regexp::Common references
248
249
# A command option that is intended to be validated by a R::C regexp is annotated with the following syntax:
250
# regexp RE::X::Y
251
# Example:
252
# port regexp RE::num::int
253
# fqdn regexp RE::net::domain
254
255
# Users also may specify custom regexps which are simply tested for syntax and precompiled.
256
# regexp
257
# Example:
258
# url regexp ^http://.*[\s*|$]
259
260
0
my $success = 1; # Failures set success to 0 and keep processing, to report the most errors
261
# possible before bailing out.
262
263
0
foreach my $option (keys %configValidation) {
264
0
0
if($configValidation{$option}{type} eq 'regexp' ) {
265
0
0
if ($configValidation{$option}{arg} =~ /^RE::(.*)\s*/) {
266
# Translate :: delimiter for search into $RE{}{} multi level hash structure.
267
0
my @levels = split /::/, $1;
268
0
$regexp = \%RE;
269
0
for my $REid (@levels) { # Thanks to bline for an elegant solution
270
0
0
if ( defined $regexp->{$REid} ) { $regexp = $regexp->{$REid}; }
0
271
else {
272
0
warn "Invalidating directive '$option': Regexp $REid does not exist within Regexp::Common";
273
0
$success = 0; # Error in the validation, treat it as a failure and block continued loading
274
}
275
};
276
}
277
else {
278
0
0
unless( eval{ qr($configValidation{$option}{arg}) } ) {
0
279
0
warn "Invalidating directive '$option': Compiling regexp ($configValidation{$option}{arg}) returned errors: $@\n";
280
0
$success = 0; # Error in the validation, treat it as a failure and block continued loading
281
}
282
}
283
}
284
}
285
0
return($success);
286
}
287
288
sub validateConfig {
289
0
0
0
my %hash = @_;
290
0
my $success = 1;
291
292
0
my @validate = keys %hash;
293
0
foreach my $key (@validate) {
294
0
0
if($configValidation{$key}) {
295
# Expand if alias.
296
0
0
if($configValidation{$key}{type} eq 'alias') {
297
0
push(@validate, $configValidation{$key}{args}); # Expand the alias, requeue for testing
298
0
next;
299
}
300
301
# bool
302
0
0
if($configValidation{$key}{type} eq 'bool') {
303
# Honestly, there's nothing to do with these, it's there or it isn't!
304
0
next;
305
}
306
307
# int
308
0
0
if($configValidation{$key}{type} eq 'int') { # int is just a shortcut to $RE{num}{int}
309
0
0
unless ($hash{$key} =~ /$RE{num}{int}/) {
310
0
warn "$key is not an integer: $hash{$key}\n";
311
0
$success = 0;
312
}
313
0
next;
314
}
315
316
# string
317
0
0
if($configValidation{$key}{type} eq 'string') {
318
0
0
unless ($hash{$key} =~ /\w+/) {
319
0
warn "$key is not an string: $hash{$key}\n";
320
0
$success = 0;
321
}
322
0
next;
323
}
324
325
# fqdn
326
0
0
if($configValidation{$key}{type} eq 'fqdn') {
327
0
0
unless ($hash{$key} =~ /$RE{net}{domain}/) {
328
0
warn "$key is not an fqnd: $hash{$key}\n";
329
0
$success = 0;
330
}
331
0
next;
332
}
333
334
# regexp
335
0
0
if($configValidation{$key}{type} eq 'regexp') {
336
0
0
unless ($hash{$key} =~ /$configValidation{$key}{args}/) {
337
0
warn "$key does not match regexp: $hash{$key}\n";
338
0
$success = 0;
339
}
340
0
next;
341
}
342
0
warn "$key has a type of $configValidation{$key}{type}, which is unrecognized.\n";
343
0
$success = 0;
344
}
345
else {
346
0
warn "Invalid config option (not declared in validation): $key \n";
347
}
348
}
349
0
return($success);
350
}
351
352
sub parseCommandLine {
353
0
0
0
my @options = @_;
354
0
my %hash;
355
356
0
while(@options) {
357
0
my $arg = shift(@options);
358
359
# Quoted string checks need to go here.
360
# As soon as I learn how to do them right.
361
362
0
0
if($arg =~ /^--(\w+)=(.*)/) { $hash{$1} = $2; }
0
0
363
0
elsif ($arg =~ /^-(\w)=(.*)/) { $hash{$1} = $2; }
364
}
365
366
0
return(%hash);
367
};
368
369
=head1 AUTHOR
370
371
Bill Nash, C<< >>
372
373
=head1 ACKNOWLEDGEMENTS
374
375
Thanks go to bline, dngor, Somni, the letter P, and the number 2.
376
377
=head1 COPYRIGHT & LICENSE
378
379
Copyright 2005 Bill Nash, All Rights Reserved.
380
381
This program is free software; you can redistribute it and/or modify it
382
under the same terms as Perl itself.
383
384
=cut
385
386
1; # End of Config::Cascade