File Coverage

blib/lib/Config/Cascade.pm
Criterion Covered Total %
statement 9 148 6.0
branch 0 90 0.0
condition 0 15 0.0
subroutine 3 9 33.3
pod 1 6 16.6
total 13 268 4.8


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             --
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             #
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             #
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