File Coverage

blib/lib/Config/Interactive.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package Config::Interactive;
2 2     2   50188 use strict;
  2         6  
  2         83  
3 2     2   11 use warnings;
  2         5  
  2         55  
4 2     2   49 use 5.006_001;
  2         6  
  2         257  
5              
6             =head1 NAME
7              
8             Config::Interactive - config module with support for interpolation, XML fragments and interactive UI
9              
10             =head1 VERSION
11              
12             Version 0.04
13              
14             =cut
15              
16             our $VERSION = '0.04';
17              
18             =head1 DESCRIPTION
19              
20             This module opens a config file and parses it's contents for you. The I method
21             accepts several parameters. The method 'parse' returns a hash reference
22             which contains all options and it's associated values of your config file as well as comments above.
23             If the dialog mode is set then at the moment of parsing user will be prompted to enter different value and
24             if validation pattern for this particular key was defined then it will be validated and user could be asked to
25             enter different value if it failed.
26             The format of config files supported by L is
27             C<< = >> pairs or XML fragments (by L, namespaces are not supported) and comments are any line which starts with #.
28             Comments inside of XML fragments will pop-up on top of the related fragment. It will interpolate any perl variable
29             which looks as C< ${?[A-Za-z]\w+}? >.
30             Please not that interpolation works for XML fragments as well, BUT interpolated varialbles MUST be defined
31             by C definition and NOT inside of other XML fragment!
32             The order of appearance of such variables in the config file is not important, means you can use C<$bar> variable anywhere in the config file but
33             set it to something on the last line (or even skip setting it at all , then it will be undef).
34             It stores internally config file contents as hash ref where data structure is:
35             Please note that array ref is used to store XML text elements and scalar for attributes.
36              
37            
38             ( 'key1' => {'comment' => "#some comment\n#more comments\n",
39             'value' => 'Value1',
40             'order' => '1',
41             },
42             'key2' => {'comment' => "#some comment\n#more comments\n",
43             'value' => 'Value2',
44             'order' => '2'
45             },
46            
47             'XMLRootKey' => {'comment' => "#some comment\n#more comments\n",
48             'order' => '3',
49             'value' => {
50             'xmlAttribute1' => 'attribute_value',
51             'subXmlKey1' => ['sub_xml_value1'],
52             'subXmlKey2' => ['sub_xml_value2'],
53             'subXmlKey3'=> ['sub_xml_value3'],
54             }
55             }
56             )
57            
58              
59             The normalized ( flat hash with only key=value pairs ) view of the config could be obtained by getNormalizedData() call.
60             All tree- like options will be flatted as key1_subkey1_subsubkey1. So the structure above will be converted into:
61              
62             ('key1' => 'Value1',
63             'key2' => 'Value2',
64             'XMLRootKey_xmlAttribute1' => 'attribute_value',
65             'XMLRootKey_subXmlKey1' => 'sub_xml_value1' ,
66             'XMLRootKey_subXmlKey2' => 'sub_xml_value2',
67             'XMLRootKey_subXmlKey3'=> 'sub_xml_value3' , )
68              
69             the case of the key will be preserved.
70              
71             =head1 SYNOPSIS
72              
73             Provides a convenient way for loading config values from a given file and
74             returns it as a hash structure, allows interpolation for the simple perl scalars C<( $xxxx ${xxx} )>
75             Also, it can run interactive session with user, use predefined prompts, use validation patterns
76             and store back into the file, preserving the order of original comments.
77             Motivation behind this module was inspired by L module which was missing required
78             functionality (preservation of the comments order and positioining, prompts and validation for
79             command line based UI ). Basically, this is I with list of features found to be useful.
80              
81             use Config::Interactive;
82            
83             # define prompts for the keys
84             my %CONF_prompts = ('username' => 'your favorite username ',
85             'password'=> 'most secure password ever ',
86             );
87            
88             my %validkeys = ('username' => ' your favorite username ',
89             'password'=> ' most secure password ever ',
90             );
91            
92             # Read in configuration information from some.config file
93            
94            
95             my $conf = Config::Interactive->new({file => 'some.conf',
96             prompts => \%CONF_prompts,
97             validkeys => \%validkeys,
98             dialog => '1'});
99             # OR
100             # set interactive mode
101             $conf->setDialog(1);
102             #
103             # use dialog prompts from this hashref
104             $conf->setPrompts(\%CONF_prompts);
105             #
106             # set delimiter
107             $conf->setDelimiter('='); # this is default delimiter
108             #
109             # use validation patterns from this hashref
110             $conf->setValidkeys(\%validkeys );
111             # parse it, interpolate variables and ask user abour username and password, validate entered values
112            
113             $conf->parse();
114            
115             # store config file back, preserving original order and comments
116             $conf->store;
117              
118              
119             =head1 METHODS
120              
121              
122             =head2 new({})
123              
124             creates new object, accepts hash ref as parameters
125              
126             Possible ways to call B:
127              
128             $conf = new Config::Interactive();
129            
130             # create object and will parse/store it within the my.conf file
131             $conf = new Config::Interactive({file => "my.conf"});
132            
133             # use current hash ref with options
134             $conf = new Config::Interactive({ file => "my.conf", data => $hashref });
135            
136             # prompt user to enter new value for every -key- which held inside of %prompts_hash
137             $conf = new Config::Interactive({ file => "my.conf", dialog => 'yes', prompts => \%promts_hash });
138            
139             # set delimiter as '?'... and validate every new value against the validation pattern
140             $conf = new Config::Interactive({ file => "my.conf", dialog => 'yes', delimiter => '?',
141             prompts => \%promts_hash, validkeys => \%validation_patterns });
142              
143             This method returns a B object (a hash blessed into C namespace.
144             All further methods must be used from that returned object. see below.
145             Please note that setting dialog option into the "true" is not enough, because the method
146             will look only for the keys defined in the C<%prompts_hash>
147             An alternative way to call B is supplying an option C<-hash> with hash reference to the set of the options.
148              
149             =over
150              
151             =item B
152              
153             prints a lot of internal stuff if set to something defined
154              
155             =item B
156              
157             name of the config file
158              
159             file => "my.conf"
160              
161              
162             =item B
163              
164             A hash reference, which will be used as the config, i.e.:
165              
166             data => \%somehash,
167              
168             where %somehash should be formatted as:
169              
170             ( 'key1' => {'comment' => "#some comment\n#more comments\n",
171             'value' => 'Value1',
172             'order' => '1',
173             },
174            
175             'key2' => {'comment' => "#some comment\n#more comments\n",
176             'value' => 'Value2',
177             'order' => '2'
178             },
179            
180             'XML_root_key' => {'comment' => "#some comment\n#more comments\n",
181             'order' => '3',
182             'value' => {
183             'xml_attribute_1' => 'attribute_value',
184             'sub_xml_key1' => ['sub_xml_value1'],
185             'sub_xml_key2' => ['sub_xml_value2'],
186             'sub_xml_key3'=> ['sub_xml_value3'],
187             }
188            
189             }
190             )
191              
192             =item B
193              
194             Set up an interactive mode, Please note that setting dialog option into the I is not enough,
195             because this method will look only for the keys defined in the C<%prompts_hash> ,
196              
197             =item B
198              
199             Default delimiter is C<=>. Any single character from this list C<= : ; + ! # ? - *> is accepted.
200             Please be careful with : since it could be part of some URL for example.
201              
202             =item B
203              
204             Hash ref with prompt text for particular -key- ,
205             where hash should be formatted as:
206              
207             ('key1' => ' Name of the key 1',
208             'key2' => 'Name of the key 2 ',
209             'key3' => ' Name of the key 3 ',
210             'sub_xml_key1' => 'Name of the key1 ',
211             'sub_xml_key2' => ' Name of the key2 ' ,
212             )
213              
214             It will reuse the same prompt for the same key name.
215            
216             =item B
217              
218             Hash ref with validation patterns for particular -key-
219             where hash should be formatted as:
220              
221             ( 'key1' => '\w+',
222             'key2' => '\d+',
223             'key3' => '\w\w\:\w\w\:\w\w\:\w\w\:\w\w\:\w\w\',
224             'sub_xml_key1' => '\d+',
225             'sub_xml_key2' => '\w+' ,
226            
227            
228             )
229              
230             It will reuse the same validation pattern for the same key name as well.
231              
232             =back
233              
234             =cut
235              
236 2     2   1930 use XML::Simple;
  0            
  0            
237             use Carp;
238             use Data::Dumper;
239             use fields qw(file debug delimiter data dialog validkeys prompts);
240            
241              
242             sub new {
243             my ($that, $param) = @_;
244             my $class = ref($that) || $that;
245             my $self = fields::new($class);
246            
247             if ($param) {
248             croak( "ONLY hash ref accepted as param and not: " . Dumper $param ) unless ref($param) eq 'HASH' ;
249             $self->{debug} = $param->{debug} if $param->{debug};
250             foreach my $key (qw/file delimiter data dialog validkeys prompts/) {
251             if($param->{$key}) {
252             $self->{$key} = $param->{$key};
253             print " Set parameter: \n" if $self->{debug};
254             }
255             }
256             }
257             $self->{delimiter} = '=' unless $self->{delimiter};
258             return $self;
259             }
260              
261             =head2 setDelimiter()
262              
263             set delimiter from the list of supported delimiters [\=\+\!\#\:\;\-\*] ,
264              
265             =cut
266              
267             sub setDelimiter {
268             my ( $self, $sep ) = @_;
269              
270             if ( !$sep || $sep !~ /^[\=\+\!\#\:\;\-\*]$/ ) {
271             croak("Delimiter is not supported or missed: $sep");
272             }
273             $self->{delimiter} = $sep;
274             return $sep;
275             }
276              
277             =head2 setDialog()
278              
279             set interactive mode (any defined value)
280             accepts: single parameter - any defined
281             returns: current state
282              
283             =cut
284              
285             sub setDialog {
286             my ( $self, $dia ) = @_;
287             $self->{dialog} = $dia;
288             return $dia;
289             }
290              
291             =head2 setFile()
292              
293             set config file name
294             accepts: single parameter - filename
295             returns: current filename
296            
297             =cut
298              
299             sub setFile {
300             my ($self, $file) = @_;
301             unless ( $file && -e $file ) {
302             croak(" File name is missing or does not exist ");
303             }
304             $self->{file} = $file;
305             return $self->{file};
306             }
307              
308             =head2 setValidkeys()
309              
310             set vaildation patterns hash
311             accepts: single parameter - reference to hash with validation keys
312             returns: reference to hash with validation keys
313            
314             =cut
315              
316             sub setValidkeys {
317             my ( $self, $vk ) = @_;
318             unless ( $vk && ref($vk) eq 'HASH' ) {
319             croak(" Validation hash ref is misssing ");
320             }
321             $self->{validkeys} = $vk;
322             return $self->{validkeys};
323             }
324              
325             =head2 setPrompts()
326              
327             set prompts hash
328             accepts: single parameter - reference to hash with prompts
329             returns: reference to hash with prompts
330            
331             =cut
332              
333             sub setPrompts {
334             my ( $self, $prompts ) = @_;
335             unless ( $prompts && ref($prompts) eq 'HASH' ) {
336             croak(" Prompts hash ref is misssing ");
337             }
338             $self->{prompts} = $prompts;
339             return $self->{prompts};
340              
341             }
342              
343             =head2 getNormalizedData()
344              
345             This method returns a normalized hash ref, see explanation above.
346             the complex key will be normalized
347             'key1' => { 'key2' => 'value' }
348             will be returned as 'key1_key2' => 'value'
349             accepts; nothing
350             returns: hash ref with normalized config data
351              
352             =cut
353              
354             sub getNormalizedData {
355             my $self = shift;
356             return _normalize( $self->{data} );
357             }
358              
359             =head2 store()
360            
361             Store into the config file, preserve all comments from the original file
362             Accepts filename as argument
363             Possible ways to call B:
364              
365             $conf->store("my.conf"); #store into the my.conf file, if -file was defined at the object creation time, then this will overwrite it
366            
367             $conf->store();
368              
369             =cut
370              
371             sub store {
372             my ( $self, $filen ) = @_;
373             my $file_to_store = ( defined $filen ) ? $filen : $self->{file};
374              
375             open OUTF, "+>$file_to_store"
376             or croak(" Failed to store config file: $file_to_store");
377             foreach my $key (
378             map { $_->[1] }
379             sort { $a->[0] <=> $b->[0] }
380             map { [ $self->{data}->{$_}{order}, $_ ] } keys %{ $self->{data} }
381             )
382             {
383             my $comment =
384             $self->{data}->{$key}{comment}
385             ? $self->{data}->{$key}{comment}
386             : "#\n";
387             my $value = (
388             $self->{data}->{$key}{pre}
389             ? $self->{data}->{$key}{pre}
390             : $self->{data}->{$key}{value}
391             );
392              
393             carp(" This option $key is : " . Dumper $value) if $self->{debug};
394            
395             if ( ref($value) eq 'HASH' ) {
396             my $xml_out = $self->{data}->{$key}{value};
397             foreach my $arg (keys %{$self->{data}->{$key}{pre}}) {
398             $xml_out->{$arg} = $self->{data}->{$key}{pre}->{$arg};
399             }
400             print OUTF $comment . XMLout( $xml_out , RootName => $key ) . "\n";
401             }
402             else {
403             print OUTF $comment . $key . $self->{delimiter} . "$value\n";
404             carp( $comment . $key . $self->{delimiter} . $value )
405             if $self->{debug};
406             }
407             }
408             close OUTF;
409             }
410              
411             =head2 parse()
412              
413             Parse config file, return hash ref ( optional)
414             Accepts filename as argument
415              
416             Possible ways to call B:
417              
418             $config_hashref = $conf->parse("my.conf"); # parse my.conf file, if -file was defined at the object creation time, then this will overwrite -file option
419            
420             $config_hashref = $conf->parse();
421            
422             This method returns a a hash ref.
423              
424             =cut
425              
426             sub parse {
427             my ( $self, $filen ) = @_;
428             my $file_to_open = ( defined $filen && -e $filen ) ? $filen : $self->{file};
429              
430             open INF, "<$file_to_open"
431             or croak(" Failed to open config file: $file_to_open");
432             print("File $file_to_open opened for parsing ") if $self->{debug};
433             my %config = ();
434             my $comment = undef;
435             my $order = 1;
436             my $xml_start = undef;
437             my $xml_config = undef;
438             my $pattern = '^([\w\.\-]+)\s*\\' . $self->{delimiter} . '\s*(.+)';
439              
440             # parsing every line from the config file, removing extra spaces
441             while () {
442             chomp;
443             s/^\s+?//;
444             if (m/^\#/xsm) {
445             $comment .= "$_\n";
446             }
447             else {
448             s/\s+$//g;
449              
450             # if not inside of XML and if this is start of XML
451             if ( !$xml_start && m/^\<\s*([\w\-]+)\b?[^\>]*\>/xsm ) {
452             $xml_start = $1;
453             $xml_config .= $_;
454             }
455             # elsif inside of XML
456             elsif ($xml_start) {
457             if (m/^\<\/\s*($xml_start)\s*\>/xsm) {
458             $xml_config .= $_;
459             my $xml_cf = XMLin( $xml_config, KeyAttr => {}, ForceArray => 1 );
460             $config{$xml_start}{value} = $self->_parseXML($xml_cf);
461             carp " Parsed XML fragment: " . Dumper $config{$xml_start}{value} if $self->{debug};
462             if ($comment) {
463             $config{$xml_start}{comment} = $comment;
464             $comment = '';
465             }
466             $config{$xml_start}{order} = $order++;
467             $xml_start = undef;
468             }
469             else {
470             $xml_config .= $_;
471             }
472             }
473              
474             # elsif outside of XML, key=value
475             elsif (m/$pattern/o) {
476             my $key = $1;
477             my $value = $2;
478             $config{$key}{value} = $self->_processKey( $key, $value );
479             $config{$key}{order} = $order++;
480             if ($comment) {
481             $config{$key}{comment} = $comment;
482             $comment = '';
483             }
484             }
485             else {
486             print(" ... Just a pattern:$pattern a string: $_")
487             if $self->{debug};
488             }
489             }
490             }
491             close INF;
492             print(" interpolating...\n") if $self->{debug};
493              
494             # interpolate all values
495              
496             $self->{data} = $self->_interpolate( \%config );
497             print( " Config data: \n" . Dumper $self->{data} ) if $self->{debug};
498             return $self->{data};
499             }
500              
501             #
502             # interpolate all values, in case of XML fragments the name of the interpolated variable
503             # MUST be set by key=value definition and not by the element from other XML block
504             #
505             #
506             sub _interpolate {
507             my ( $self, $config, $scalars, $xml_root ) = @_;
508             my @keys = $xml_root ? keys %{ $config->{value} } : keys %{$config};
509              
510             # interpolate all values
511             foreach my $key (@keys) {
512             ### go for recursion in case of XML fragment
513             if ( !$xml_root ) {
514             $self->_interpolate( $config->{$key}, $config, $key )
515             if ref( $config->{$key}{value} ) eq 'HASH';
516             ### interpolate if its simple key=value definition
517             my @sub_keys =
518             $config->{$key}{value} =~ /[^\\]?\$\{?([a-zA-Z]+(?:\w+)?)\}?/xsmg;
519             foreach my $sub_key (@sub_keys) {
520             print(
521             " CHECK " . $config->{$key}{value} . " -> $sub_key \n" )
522             if $self->{debug};
523             if ( $sub_key && $config->{"$sub_key"} ) {
524             my $subst = $config->{"$sub_key"}{value};
525             $config->{$key}{pre} =
526             $config->{$key}{pre}
527             ? $config->{$key}{pre}
528             : $config->{$key}{value};
529             $config->{$key}{value} =~ s/\$\{?$sub_key\}?/$subst/xsmg;
530             carp( " interpolated "
531             . $config->{$key}{value}
532             . " -> $sub_key -> $subst \n" )
533             if $self->{debug};
534             }
535             }
536             }
537             else {
538             ## XML keys located under the value key and its single size array in case of element and just scalar for attr
539             my $xml_value =
540             ref( $config->{value}{$key} ) eq 'ARRAY'
541             ? $config->{value}{$key}->[0]
542             : $config->{value}{$key};
543              
544             my @sub_keys =
545             $xml_value =~ /[^\\]?\$\{?([a-zA-Z]+(?:\w+)?)\}?/xsmg;
546             foreach my $sub_key (@sub_keys) {
547             print( " CHECK " . $xml_value . " -> $sub_key \n" )
548             if $self->{debug};
549             if ( $sub_key && $scalars->{"$sub_key"} ) {
550             my $subst = $scalars->{"$sub_key"}{value};
551             if ( ref( $config->{value}{$key} ) eq 'ARRAY' ) {
552             $config->{pre}{$key}->[0] =
553             $config->{pre}{$key}->[0]
554             ? $config->{pre}{$key}->[0]
555             : $config->{value}{$key}->[0];
556              
557             $config->{value}{$key}->[0] =~
558             s/\$\{?$sub_key\}?/$subst/xsmg;
559             }
560             else {
561             $config->{pre}{$key} =
562             $config->{pre}{$key}
563             ? $config->{pre}{$key}
564             : $config->{value}{$key};
565              
566             $config->{value}{$key} =~
567             s/\$\{?$sub_key\}?/$subst/xsmg;
568             }
569             carp( " interpolated "
570             . $xml_value
571             . " -> $sub_key -> $subst \n" )
572             if $self->{debug};
573             }
574             }
575             }
576             }
577             return $config;
578             }
579              
580             #
581             # enter prompt on the screen
582             #
583             #
584             sub _promptEnter {
585             my $prompt = shift;
586             print "$prompt\n";
587             my $entered = ;
588             chomp $entered;
589             $entered =~ s/\s+//g;
590             return $entered;
591             }
592              
593             #
594             # recursive walk through the XML::Simple tree
595             #
596              
597             sub _parseXML {
598             my ( $self, $xml_cf ) = @_;
599              
600             foreach my $key ( keys %{$xml_cf} ) {
601             if ( ref( $xml_cf->{$key} ) eq 'HASH' ) {
602             $xml_cf->{$key} = $self->_parseXML( $xml_cf->{$key} );
603             }
604             elsif ( ref( $xml_cf->{$key} ) eq 'ARRAY' ) {
605             $xml_cf->{$key}->[0] =
606             $self->_processKey( $key, $xml_cf->{$key}->[0] );
607             }
608             else {
609             $xml_cf->{$key} = $self->_processKey( $key, $xml_cf->{$key} );
610             }
611             }
612             return $xml_cf;
613             }
614              
615             #
616             # keys normalization
617             # 'value' = > { 'key0' => ['value0'], 'key1' => { 'key12' => ['value12' ]}, 'key2' => { 'key22' => ['value22' ]}}
618             #
619              
620             sub _normalize {
621             my ( $data, $parent ) = @_;
622             my %new_data = ();
623              
624             foreach my $key ( keys %{$data} ) {
625             my $new_key = $parent ? "$parent\_$key" : $key;
626              
627             my $value = $data->{$key};
628             if ( ref($value) eq 'HASH'
629             && $value->{value}
630             && ref( $value->{value} ) eq 'HASH' )
631             {
632             %new_data =
633             ( %new_data,
634             %{ _normalize( $data->{$key}->{value}, $new_key ) } );
635             }
636             elsif ( ref($value) eq 'ARRAY' ) {
637             $new_data{$new_key} = $data->{$key}->[0];
638             }
639             elsif ( ref($value) eq 'HASH' && $value->{value} ) {
640             $new_data{$new_key} = $value->{value};
641             }
642             elsif ( ref($value) eq 'HASH' && !$value->{value} ) {
643             $new_data{$new_key} = 0;
644             }
645             else {
646             $new_data{$new_key} = $value;
647             }
648             }
649             return \%new_data;
650             }
651              
652             #
653             # processing each key entered from the screen
654             #
655             #
656              
657             sub _processKey {
658             my ( $self, $key, $value ) = @_;
659             $value =~ s/^\s+//;
660             $value =~ s/\s+$//;
661             my $vpattern =
662             ( $self->{validkeys} && $self->{validkeys}->{$key} )
663             ? qr/$self->{validkeys}->{$key}/
664             : undef;
665             my $pkey =
666             ( $self->{prompts} && $self->{prompts}->{$key} )
667             ? $self->{prompts}->{$key}
668             : undef;
669              
670             if ( $self->{dialog} && $pkey ) {
671             my $entered = _promptEnter(
672             " Please enter the value for the $pkey (Default is $value)>");
673             while ( $entered && ( $vpattern && $entered !~ $vpattern ) ) {
674             $entered = _promptEnter(
675             "!!! Entered value is not valid according to regexp: $vpattern , please re-enter>"
676             );
677             }
678             $value = $entered ? $entered : $value;
679             }
680             if ( $vpattern && $value !~ $vpattern ) {
681             croak(
682             "Parser failed, value:$value for $key is NOT VALID according to pattern: $vpattern"
683             );
684             }
685              
686             return $value;
687              
688             }
689              
690             1;
691              
692             __END__
693              
694              
695             =head1 DEPENDENCIES
696              
697             L, L, L
698              
699              
700             =head1 EXAMPLES
701              
702              
703             For example this config file:
704              
705            
706             # username
707             USERNAME = user
708             PASSWORD = pass
709             #sql config
710            
711            
712             mysql
713            
714            
715             database
716            
717            
718              
719             =head1 SEE ALSO
720              
721             L
722              
723             =head1 AUTHOR
724              
725             Maxim Grigoriev , 2007-2008, Fermilab
726              
727             =head1 COPYRIGHT
728              
729             Copyright(c) 2007-2008, Fermi Reasearch Alliance (FRA)
730              
731             =head1 LICENSE
732              
733             You should have received a copy of the Fermitools license
734             with this software. If not, see L
735              
736             =cut